Skip to content

fix(bin): wrap long meta-row badges in the generated kanban board - #5

Merged
Lcxiv merged 2 commits into
mainfrom
fm/fm-board-badge-clip
Aug 1, 2026
Merged

Lcxiv merged 2 commits into
mainfrom
fm/fm-board-badge-clip

Conversation

@Lcxiv

@Lcxiv Lcxiv commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Intent

Fix vertically clipped badge text in the kanban board generated by bin/fm-board.sh. The board renders card metadata as DaisyUI badges inside .meta-row; DaisyUI's .badge has a fixed height and does not wrap, so a badge whose text runs longer than one line has its text clipped. A real browser layout audit of a generated board reported two severe clipped-text failures (vertical, 6.5px overflow, viewport 1152): the 'fog:' and 'out of scope:' badges on an effort-map card in the top band. Their text is long by nature - the generator already truncates it with an ellipsis and even truncated it exceeds one badge line. The captain asked to fix the CLASS of defect (any long .meta-row badge), not just those two spans.

Deliberate decisions, so a reviewer reading only the diff has the context:

  • This is a PRESENTATION-LAYER fix only. Explicitly out of scope by the captain's instruction: do NOT change the truncation logic and do NOT change the badge markup shape. The ellipsis truncation staying in place is intentional, not an oversight - the fix makes the already-truncated text wrap and be fully readable rather than clipped.
  • The fix is deliberately scoped to '.meta-row .badge' and must NOT restyle badges elsewhere; the header count badges are short and already render correctly, so widening the selector would be a regression.
  • The exact rule (height:auto; min-height:1.1rem; white-space:normal; text-align:left; line-height:1.3; padding-top/bottom:0.1rem) is proven-working evidence: it was applied by hand to a generated artifact and confirmed by a clean re-audit in a real browser before being mirrored into the generator. It is placed directly beside the existing .meta-row rule to match the surrounding style block's conventions.
  • The board must remain a single self-contained HTML file with no new external assets, so the fix is an inline stylesheet rule rather than any added asset.

Test coverage: tests/fm-board.test.sh gains an assertion inside the existing render test that extracts the emitted .meta-row .badge rule from the GENERATED HTML (the executable interface, per this repo's rule that tests must never assert implementation-source bytes) and checks height:auto and white-space:normal, so the defect cannot silently return. It matches the file's existing assert_contains conventions rather than adding a new runner.

Verification already performed: the full tests/fm-board.test.sh suite passes (4/4). A board generated from live fleet data was opened in a real browser and all 60 .meta-row badges measured - zero clipped; the previously-clipped fog badge now renders 31px tall against 29px of content, wrapping to two lines. docs/board.md was checked and documents no styling behavior, so per the task it was correctly left untouched.

Two established environment facts: (1) local lint cannot run on this machine - the pinned shellcheck is linux-x86_64 only and this is Apple Silicon macOS; bin/fm-lint.sh correctly refuses. (2) origin is the captain's own fork Lcxiv/firstmate with push access, and that fork has no CI configured, so no GitHub Actions run will ever appear for this PR.

What Changed

  • Added a .meta-row .badge rule to the inline stylesheet emitted by bin/fm-board.sh (height:auto; min-height:1.1rem; white-space:normal; text-align:left; line-height:1.3; plus small vertical padding), so card metadata longer than one line wraps instead of being vertically clipped by DaisyUI's fixed-height, no-wrap .badge. The selector is deliberately scoped to .meta-row so header count badges keep their current rendering, and the rule is inline so the board stays a single self-contained HTML file.
  • Left the existing ellipsis truncation and badge markup untouched — even truncated, fog: and out of scope: text on effort-map cards exceeds one badge line, so this is a presentation-layer fix on top of truncation, not a replacement for it.
  • Extended the existing render test in tests/fm-board.test.sh to extract the emitted .meta-row .badge rule from the generated HTML and assert height:auto and white-space:normal, and documented the invariant in the bin/fm-board.sh header comment beside the existing clipping-bug note. The full board suite passes 4/4, and a browser layout audit of a generated board measured zero clipped .meta-row badges after the change.

Risk Assessment

✅ Low: A single presentation-only CSS rule tightly scoped to .meta-row .badge, with no logic, markup, or data-path changes, plus a generated-HTML regression assertion that fails correctly if the rule is removed.

Testing

Ran the targeted suite tests/fm-board.test.sh (4/4 pass) and verified its new assertion actually catches the defect by removing the CSS rule and watching it fail. For end-user evidence I generated a real board with long effort-map metadata, produced a pre-fix baseline of the identical HTML minus the one new rule, and opened both in Chrome: before the fix 1-2 .meta-row badges clip with 8px of vertical overflow depending on viewport, after the fix zero of 12 clip at 1600, 1152, and 500px, with the fog badge growing to 31px and wrapping to two fully readable lines. The 1600px pair also shows a ticket card's stage: badge — not one of the two originally reported spans — going from clipped to wrapped, which is the direct evidence that the class of defect is fixed rather than the two instances. Header count badges measured an unchanged 24px fixed height throughout, and the rendered output still shows the ellipsis truncation, so the scoping and out-of-scope constraints both hold. Screenshots and the rendered board are in the evidence directory; the worktree is clean and temp fixtures were removed.

  • Evidence: Before fix @1152 — fog and out-of-scope badge text spills outside the badge box and collides with the neighbouring badge (local file: /var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/01-before-clipped-badges.png)
  • Evidence: After fix @1152 — both long badges grow to two lines, fully readable inside their own badge boxes; header count badges unchanged (local file: /var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/02-after-wrapped-badges.png)
  • Evidence: Before fix @1600 — a ticket card's long 'stage: unknown - harness state unavailable' badge clips (class of defect, not one of the two reported spans) (local file: /var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/03-before-1600-ticket-badge-clipped.png)
  • Evidence: After fix @1600 — the same ticket-card badge wraps cleanly to two lines (local file: /var/folders/gp/phywrkfd3kj2jp2f764xmldw0000gn/T/no-mistakes-evidence/01KYWFCVGYZXGXP8SP4JH2HFAJ/04-after-1600-ticket-badge-wrapped.png)
Evidence: Generated board HTML used for the post-fix measurements (self-contained, no added assets)
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fleet Board — 2026-07-31</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<style>
  .col { min-width: 0; }
  .card-compact .card-body { padding: 0.85rem; }
  .ticket { border-left: 4px solid transparent; overflow: visible; }
  .card, .card-body, .todos, .todos li, details, summary { overflow: visible !important; max-height: none !important; height: auto !important; }
  .t-captain { border-left-color: oklch(75% 0.18 60); }
  .t-flight  { border-left-color: oklch(70% 0.15 250); }
  .t-queued  { border-left-color: oklch(70% 0.12 180); }
  .t-upstream{ border-left-color: oklch(72% 0.14 300); }
  .t-done    { border-left-color: oklch(72% 0.17 145); }
  .todos { font-size: 0.72rem; line-height: 1.35; }
  .todos li { list-style: none; }
  .meta-row { display:flex; flex-wrap:wrap; gap:0.3rem; margin:0.25rem 0; }
  .meta-row .badge { height:auto; min-height:1.1rem; white-space:normal; text-align:left; line-height:1.3; padding-top:0.1rem; padding-bottom:0.1rem; }
  .approval { background: oklch(30% 0.06 60 / 0.4); border: 1px solid oklch(75% 0.18 60 / 0.7); border-radius: 0.4rem; padding: 0.25rem 0.45rem; margin: 0.25rem 0; }
  .approval summary { font-weight: 700; color: oklch(85% 0.16 70); cursor: pointer; font-size: 0.7rem; }
  .approval textarea { width:100%; font-size:0.7rem; margin-top:0.2rem; }
  .approval label { display:block; font-size:0.7rem; padding:0.1rem 0; cursor:pointer; }
  .approval .queued-note { display:none; font-size:0.7rem; color: oklch(80% 0.15 145); font-weight:600; }
  .dragging { opacity: 0.4; }
  .col.dropover { outline: 2px dashed oklch(70% 0.15 250); outline-offset: 4px; border-radius: 0.5rem; }
</style>
</head>
<body class="bg-base-200 min-h-screen p-4 md:p-6">
<div class="max-w-[1700px] mx-auto">
  <header class="mb-5 flex flex-wrap items-end justify-between gap-2">
    <div>
      <h1 class="text-2xl font-bold">Fleet Board</h1>
      <p class="text-sm opacity-70">snapshot 2026-07-31T18:42:54Z · regenerate with bin/fm-board.sh; say “refresh the board” anytime</p>
    </div>
    <div class="flex gap-2 text-xs"><span class="badge badge-warning badge-outline">Your call: 0</span><span class="badge badge-accent badge-outline">Queued: 1</span><span class="badge badge-info badge-outline">Building: 1</span><span class="badge badge-secondary badge-outline">Review: 0</span><span class="badge badge-success badge-outline">Landed: 0</span></div>
  </header>
  <div class="mb-4 grid grid-cols-1 md:grid-cols-2 gap-3"><div class="card card-compact bg-base-100 shadow border-l-4" style="border-left-color: oklch(78% 0.14 200);"><div class="card-body"><h3 class="font-semibold text-sm">🗺️ Effort: alpha refit</h3><p class="text-xs opacity-80"><b>Destination:</b> Alpha sails clean: refit landed and every mast decision settled.</p><div class="meta-row"><span class="badge badge-xs badge-success">2 decided</span><span class="badge badge-xs badge-warning">1 open</span><span class="badge badge-xs badge-ghost">fog: rigging vendor selection and the lead time it implies; whether the spare halyard set ships with the refit or follows la…</span><span class="badge badge-xs badge-ghost">out of scope: new hull fabrication and the yard time it would require; migrating the legacy navigation stack off its current chartplo…</span></div></div></div></div>
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-4">
<section class="col" ondragover="event.preventDefault(); this.classList.add('dropover')" ondragleave="this.classList.remove('dropover')" ondrop="fbDrop(event, this)"><h2 class="font-semibold mb-2 text-warning">1 · ⚓ Decide</h2><div class="flex flex-col gap-3"><div class="text-xs opacity-50 italic p-2">nothing here</div></div></section><section class="col" ondragover="event.preventDefault(); this.classList.add('dropover')" ondragleave="this.classList.remove('dropover')" ondrop="fbDrop(event, this)"><h2 class="font-semibold mb-2 text-accent">2 · 🧭 Queued</h2><div class="flex flex-col gap-3"><div draggable="true" ondragstart="fbDrag(event, this)" ondragend="this.classList.remove('dragging')" class="card card-compact bg-base-100 ticket t-queued shadow" data-board-id="queued-task"><div class="card-body"><h3 class="font-semibold text-sm">Queued Chore</h3><div class="meta-row"><span class="badge badge-xs badge-ghost">alpha</span><span class="badge badge-xs badge-ghost">ship</span><span class="badge badge-xs badge-ghost">stage: blocked by build-task</span><span class="badge badge-xs badge-warning badge-outline">blocked-by: build-task</span></div></div></div></div></section><section class="col" ondragover="event.preventDefault(); this.classList.add('dropover')" ondragleave="this.classList.remove('dropover')" ondrop="fbDrop(event, this)"><h2 class="font-semibold mb-2 text-info">3 · ⛵ Building</h2><div class="flex flex-col gap-3"><div draggable="true" ondragstart="fbDrag(event, this)" ondragend="this.classList.remove('dragging')" class="card card-compact bg-base-100 ticket t-flight shadow" data-board-id="build-task"><div class="card-body"><h3 class="font-semibold text-sm">Build The Widget</h3><div class="meta-row"><span class="badge badge-xs badge-ghost">alpha</span><span class="badge badge-xs badge-ghost">ship</span><span class="badge badge-xs badge-info badge-outline">crew: codex · opus · high effort</span><span class="badge badge-xs badge-ghost">stage: unknown - harness state unavailable (unknown codex-unverified)</span></div><ul class="todos opacity-80"><li>Widget assembly in progress.</li></ul></div></div></div></section><section class="col" ondragover="event.preventDefault(); this.classList.add('dropover')" ondragleave="this.classList.remove('dropover')" ondrop="fbDrop(event, this)"><h2 class="font-semibold mb-2 text-secondary">4 · 🌊 Review</h2><div class="flex flex-col gap-3"><div class="text-xs opacity-50 italic p-2">nothing here</div></div></section><section class="col" ondragover="event.preventDefault(); this.classList.add('dropover')" ondragleave="this.classList.remove('dropover')" ondrop="fbDrop(event, this)"><h2 class="font-semibold mb-2 text-success">5 · 🏁 Landed</h2><div class="flex flex-col gap-3"><div class="text-xs opacity-50 italic p-2">nothing here</div></div></section>
  </div>
  <footer class="mt-6 text-xs opacity-60">Static snapshot — answer Decide cards or drag tickets, then press Send to Agent; firstmate dispatches from this board.</footer>
</div>
<script>
let fbDragged=null;
function fbDrag(ev, el){ fbDragged=el; el.classList.add('dragging'); ev.dataTransfer.effectAllowed='move'; }
function fbDrop(ev, colEl){
  ev.preventDefault(); colEl.classList.remove('dropover');
  if(!fbDragged) return;
  const ticket=(fbDragged.querySelector('h3')||{}).innerText||'ticket';
  const stage=(colEl.querySelector('h2')||{}).innerText||'column';
  colEl.querySelector('.flex.flex-col').appendChild(fbDragged);
  if(window.lavish && window.lavish.queuePrompt){
    window.lavish.queuePrompt('Move ticket "'+ticket.trim()+'" to stage "'+stage.trim()+'" - advance it accordingly', { tag:'move', text:ticket.trim()+' -> '+stage.trim(), element: fbDragged, queueKey:'move-'+ticket.trim() });
  }
  fbDragged=null;
}
function fbDecide(ev){
  ev.preventDefault();
  const form=ev.currentTarget;
  const key=form.dataset.question;
  const fd=new FormData(form);
  const c=((fd.get('freetext')||'').trim()) || fd.get(key);
  if(c && window.lavish && window.lavish.queuePrompt){
    window.lavish.queuePrompt('Decision '+key+': '+c, { tag:'choice', text:key+': '+c, element: form, queueKey:key, data:{question:key, answer:c} });
    form.querySelector('.queued-note').style.display='block';
  }
  return false;
}
</script>
</body>
</html>
Evidence: Browser clipping measurements across three viewports (scrollHeight - clientHeight over every .meta-row .badge)
before @1600 -> {"total":12,"clippedCount":1,"clipped":["stage: unknown - harness state unavailab [+8px]"],"headerBadgeHeights":[24,24,24,24,24]}
before @1152 -> {"total":12,"clippedCount":2,"clipped":["fog: rigging vendor selection and the le [+8px]","out of scope: new hull fabrication and t [+8px]"],"headerBadgeHeights":[24,24,24,24,24]}
before @500 -> {"total":12,"clippedCount":2,"clipped":["fog: … [+8px]","out of scope: … [+8px]"],"headerBadgeHeights":[24,24,24,24,24]}
after @1600 -> {"total":12,"clippedCount":0,"clipped":[],"headerBadgeHeights":[24,24,24,24,24]}
after @1152 -> {"total":12,"clippedCount":0,"clipped":[],"headerBadgeHeights":[24,24,24,24,24]}
after @500 -> {"total":12,"clippedCount":0,"clipped":[],"headerBadgeHeights":[24,24,24,24,24]}

fog badge after fix: height 31px, scrollHeight 29, clientHeight 29, overflow 0
Evidence: New assertion fails when the fix is removed (non-vacuity proof)
$ sed -i '' '/^ \.meta-row \.badge {{/d' bin/fm-board.sh && bash tests/fm-board.test.sh
not ok - meta badges must grow past one line (missing: 'height:auto')

$ git checkout bin/fm-board.sh && bash tests/fm-board.test.sh
ok - board renders all five columns, cards, decision panels, and effort maps
ok - captain holds route to Decide and PR-recorded work routes to Review
ok - approval panels are siblings of the todo list, never nested in it
ok - default output path works and an empty fleet renders explicit placeholders

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 1 issue found → auto-fixed ✅
  • ⚠️ .agents/skills/afk/SKILL.md - branch carries 20 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (59 file(s)) into the PR:
  • 2aeff5e Kanban dispatch board: bin/fm-board.sh + docs + tests (Kanban dispatch board: bin/fm-board.sh + docs + tests #4)
  • 15091bc Merge fork main (effort maps, lock fix, hermes adapter); conflicts resolved to the fork's reworked versions
  • c4454fb Merge pull request Hermes worker adapter (verified) #3 from Lcxiv/fm/fm-hermes-adapter
  • 30b2d4e Merge pull request Session-lock fork-handover fix (panel-hardened) #2 from Lcxiv/fm/fm-lock-survives-session-fork
  • f74208e Merge pull request Effort maps: wayfinder-style orientation maps #1 from Lcxiv/fm/fm-wayfinder-maps
  • 77b6115 no-mistakes(ci): port Hermes watcher busy expectations to the semantic busy-state contract
  • e135055 no-mistakes(document): document Hermes turn-end token in AGENTS.md state inventory
  • 29115c2 no-mistakes(test): scrub inherited agent env markers in Hermes detection test
  • 7f47f83 no-mistakes(review): fix raw-launch Hermes token registry creation; add regression test
  • 51f719e feat(harness): add Hermes worker adapter
  • cfa5660 feat(skills): add effort-maps orientation convention for multi-session efforts
  • f6c815b fix(session-lock): keep an identical sidecar in place across a same-session re-key
  • cb22cfb fix(session-lock): cover the id-minting fork end-to-end and state re-key scope honestly
  • c76f9f4 fix(session-lock): let a fork or relaunch successor own its home, loudly
  • 810129e no-mistakes(document): correct Claude session-identity docs for versioned exec and fail-closed inertness
  • fe154b5 no-mistakes(review): anchor Claude subcommand extraction on the executable token
  • 2e9602a fix(session-lock): never use Claude's shared daemon as session identity
  • acae49e no-mistakes(document): correct Claude session-identity docs for versioned exec and fail-closed inertness
  • 15bc673 no-mistakes(review): anchor Claude subcommand extraction on the executable token
  • 538b44b fix(session-lock): never use Claude's shared daemon as session identity

Push main to origin, or rebase your branch onto origin/main, before gating.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-board.sh:428 - bin/fm-board.sh:428 — white-space:normal only breaks at whitespace, so a .meta-row badge whose text is a single unbreakable token wider than the card (e.g. a URL landing in an effort-map fog/out-of-scope item) would overflow horizontally rather than wrap. No current badge source produces such a token (fog/out-of-scope are joined with "; ", stage: is prose, repo/kind/crew/blocked-by are short), so this is not a reachable defect today — adding overflow-wrap:anywhere to the same rule would close the residual case if it ever appears. Flagged as an observation only; it does not affect the reported clipping class.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-board.test.sh — full board suite, 4/4 pass
  • Non-vacuity check: deleted the .meta-row .badge line from bin/fm-board.sh, re-ran bash tests/fm-board.test.sh → not ok - meta badges must grow past one line (missing: &#39;height:auto&#39;), then git checkout bin/fm-board.sh
  • Generated a board via FM_HOME=&lt;fixture&gt; bin/fm-board.sh --out after.html from a fixture whose effort map has long Not yet specified / Out of scope entries, so fog: and out of scope: truncate to more than one badge line
  • Built the pre-fix baseline by stripping only the .meta-row .badge rule from the same generated HTML (grep -v → before.html; diff confirms a 1-line delta)
  • Opened both files in real Chrome via chrome-devtools-axi open file://… and measured every .meta-row .badge with scrollHeight - clientHeight at viewports 1600, 1152, and 500 — before: 1/2/2 clipped at +8px; after: 0/0/0
  • Measured header .badge computed height in every before/after pair (24px in all cases) to confirm the fix does not restyle badges outside .meta-row
  • Captured chrome-devtools-axi screenshot before/after pairs at 1152 (effort-map fog and out-of-scope badges) and 1600 (a ticket card's long stage: badge)
✅ **Document** - passed

✅ No issues found.

⏭️ **Lint** - skipped
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

Louis Condevaux added 2 commits July 31, 2026 09:14
DaisyUI's .badge has a fixed height and does not wrap, so a card's
metadata badge whose text runs past one line has that text clipped.
The effort-map card's fog: and out of scope: badges hit this today,
but any long .meta-row badge can.

Add a stylesheet override in the generated board that lets .meta-row
badges grow and wrap. Presentation only: truncation logic and badge
markup are unchanged, and badges outside .meta-row keep their default
fixed height.
@Lcxiv
Lcxiv merged commit 30b634c into main Aug 1, 2026
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