master
Drew Owens Publish 0.60.0 6600ce8

adom-gantt 0.57.0: header cleanup, collapse all, wrapping legend, sticky text size

Date: 2026-09-29. Requested by Drew. A guide, not a contract: if an assumption below is wrong, do the smarter thing and record it under Build notes.

Working copy

  • Source: /home/adom/project/adom-gantt (files-only clone of adom/adom-gantt 0.56.0). The app is gantt.html (one file, about 10k lines) served by server.js.
  • Real plan to exercise: /path/to/launch-plan.json (20 phases, 114 items, 2026-09-21 to 2027-01-14, grain week). Never write to that file or beside it; copy it to a temp dir first, since the server writes sidecars.
  • Run: node server.js --data <copy>.json --port <port> --no-auth.
  • Test tooling already in place: test/node_modules/puppeteer-core and Chrome at /home/adom/.cache/puppeteer/chrome/linux-*/chrome-linux64/chrome (launches with --no-sandbox --disable-gpu --disable-dev-shm-usage).

Behaviour wanted

  1. Collapse / expand all sections. One control that collapses every phase lane and, when all are already collapsed, expands them all. Its label or icon and tooltip say which of the two it will do. It reuses the existing per-lane collapse (collapsed, prefs.collapsed, applyCollapse), so the state persists like a per-lane collapse does and both lenses (chart, table) agree. A mixed state (some collapsed) needs a defined answer.

  2. Text size persists across a refresh. The A-/A+ control writes prefs.fs to localStorage, yet Drew sees the size reset on refresh. Find the real cause before changing anything. Candidates: the exported HTML snapshot sets SNAPSHOT = true and savedPrefs() then returns {}, so nothing a reader sets survives there; project.view defaults; something re-clamping fs. Text size is a statement about the reader, not the author, so it should survive a refresh in the live app AND in an exported snapshot, while the snapshot keeps opening with the author's view for everything else.

  3. Header declutter (first screenshot). The name in the brand block at the far left stays. Remove the bold repeat of the project title (.subject-name) and the dimmed description (.subject-sub) from the header. project.title and project.subtitle stay valid data fields; they just stop being painted into the header (the title can remain in document.title and the brand tooltip). If a plan has no appName, the brand should show the project title rather than the literal "adom-gantt".

  4. Start date is always an exact date. The starts button currently shows a month ("Sep 2026") at any grain except day. It must show the full date at every grain.

  5. Replace the cell count. "25 weeks" is the number of axis cells at the current grain. It changes with the grain picker, says nothing about the project, and contradicted the plan's own 16-week statement. Replace it with two readouts:

    • Project Duration: N from the plan's first date to its last date.
    • Time Remaining: M from today to the plan's last date. Both must be independent of the grain picker, update live when dates change (drag, editor, table edit, start shift, auto-schedule, undo, redo, paste, delete), and have defined answers for a plan that has not started, a plan that has ended, and an empty plan. Choose a unit format a person can read at a glance and make the tooltip state the exact dates it was computed from.
  6. Legend wraps (second screenshot). With 20 phases the legend clips at the pane edge (flex-wrap: nowrap; overflow: hidden). It must wrap onto as many lines as it needs so every phase chip is visible and clickable. Anything that measured the old single-line height (sticky axis, pinned section header, scroll-follow maths, the table's sticky header) must keep working when the legend is two or three lines tall and when it changes height on resize or on a text-size change.

Tests (app-testing skill)

The repo has no test suite. Build one under test/ following ~/.claude/skills/app-testing/SKILL.md:

  • Self-hosted: spawn server.js --no-auth on a scratch port against a temp copy of seeded data; clean up always.
  • Real Chrome, page-error gate on every page.
  • Flow tests asserting the EFFECT of each of the six behaviours above, plus the existing journeys they could break (per-lane collapse, legend filter click, grain change, text size steps, start-date picker opens).
  • Coverage gate: a manifest of every control the app renders in every state, no orphans, every required control present, click every safe control.
  • Viewport matrix including 393px wide and a desktop width; no sideways page scroll.
  • Today's date must be deterministic in tests (pin the clock in the page).
  • node test/ui_test.mjs exits non-zero on any failure.

Housekeeping

  • Bump to 0.57.0 (package.json, and wherever else the version is stated).
  • Update SKILL.md and README.md where they describe the header, the text size, collapse, and the legend. House style: no em dashes in prose.
  • Append to docs/BUILD-NOTES.md in its existing style.
  • test/node_modules must not ship: keep it out of the package and ignored.
  • Do NOT publish to the wiki and do NOT touch /home/adom/project/adom_modules. Publishing is Drew's call.

Build notes

Build notes - 2026-09-29

What changed against this plan, and why. The full account is the 0.57.0 entry at the top of docs/BUILD-NOTES.md.

  1. Text size had two root causes, not one. The snapshot suspect was right: in an exported snapshot savedPrefs() returned {}, so the size was written and never read (reproduced: 120% became 100% on every reload). The live app was NOT clean, though: with a second tab open, that tab's next save of any pref wrote its stale fs back over the new one (reproduced: B at 120%, A changes grain, B reloads at 100%). Fixed both: READER_PREFS (a snapshot reads and writes only fs, merged into what is stored) and a storage listener that keeps every open tab's fs current. Side effect worth having: a snapshot no longer overwrites a live app's grain, lens and collapse set on a shared origin.
  2. The readouts are stacked, not inline. Inline, "Time Remaining" was cut off mid-word at 1440px (seen in a screenshot). Two stacked lines fit. The version hides below 1500px (it is on the brand tooltip now) and the labels shorten to "Duration" / "Remaining" below 1240px. The plan's labels are kept verbatim at desktop widths.
  3. Duration counts both ends (a one-day plan is 1 day), matches the Analytics span, and includes cancelled rows, because the start button already does. Format: days, then 16w 4d, then months, then years; exact counts on the tooltips.
  4. Collapse all lives at the head of the legend, not in the header: the header has no room, the per-lane chevrons exist only in the chart, and the legend is present in both the chart and the table, so one control serves both lenses. Mixed state collapses; empty lanes do not count towards "all collapsed".
  5. Nothing measured the legend's height. The plan listed things that might; reading the code, all of them are relative to their own scroll boxes, and the layout is flex. So the fix is CSS only, and the suite measures the sticky geometry to prove it rather than trusting the reading.
  6. Beyond the brief: the header now wraps below 1180px (it already ran off the right edge at 1100px and at 393px in 0.56.0, and the longer start date made that worse), the table's filters wrap on a phone, and the month picker clamps the day (the 31st moved to February used to land on 3 March; an exact start label would now show that).
  7. Test oracle is the server. The app keeps its state in one closure, so the suite cannot read items; it reads /export/json after each edit's save lands and computes the expected readouts itself. That makes each live-update check end to end.
  8. BUILD-NOTES.md is newest first, so the 0.57.0 entry went at the top rather than at the tail.
  9. .adomignore added (plus .gitignore lines) to keep test/node_modules/ and test/out/ out of the package. The .adomignore syntax is assumed to be gitignore-like; check it on the first publish.

Round 1 review fixes

  1. Header layout is measured, not breakpointed. The first build's fixed breakpoints left the readouts clipped at 1181 to about 1390px and elsewhere. fitHeader() sheds version, button words, long labels, then wraps, choosing the first state whose content fits; the readouts never shrink. Swept in the suite 360 to 2000px, with and without appName, at 100% and 150%.
  2. Prefs are merge-on-write for every key, not just the size, so no stale tab can undo another tab's collapse, theme or grain.
  3. Cancelled rows are left out of Duration and Remaining (the plan's first reading had them in, for consistency with the start button; consistency with Auto-schedule, the critical path and the hours is the better rule). All-cancelled reads none.
  4. Not-started is shown on the readout, not only on the tooltip; Remaining still counts from today.
  5. A print-only title line, since the header no longer names the plan in print.