Adom Gantt
Public Made by Adomby adom
Interactive Gantt chart viewer for project timelines, with a table view and an analytics page over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. One click auto-schedules the whole plan so every dependency lines up finish-to-start on working days, skipping weekends and US federal holidays, push
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 byserver.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-coreand Chrome at/home/adom/.cache/puppeteer/chrome/linux-*/chrome-linux64/chrome(launches with--no-sandbox --disable-gpu --disable-dev-shm-usage).
Behaviour wanted
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.Text size persists across a refresh. The A-/A+ control writes
prefs.fsto localStorage, yet Drew sees the size reset on refresh. Find the real cause before changing anything. Candidates: the exported HTML snapshot setsSNAPSHOT = trueandsavedPrefs()then returns{}, so nothing a reader sets survives there;project.viewdefaults; something re-clampingfs. 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.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.titleandproject.subtitlestay valid data fields; they just stop being painted into the header (the title can remain indocument.titleand the brand tooltip). If a plan has noappName, the brand should show the project title rather than the literal "adom-gantt".Start date is always an exact date. The
startsbutton currently shows a month ("Sep 2026") at any grain except day. It must show the full date at every grain.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: Nfrom the plan's first date to its last date.Time Remaining: Mfrom 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.
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-authon 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.mjsexits non-zero on any failure.
Housekeeping
- Bump to 0.57.0 (
package.json, and wherever else the version is stated). - Update
SKILL.mdandREADME.mdwhere they describe the header, the text size, collapse, and the legend. House style: no em dashes in prose. - Append to
docs/BUILD-NOTES.mdin its existing style. test/node_modulesmust 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.
- 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 stalefsback over the new one (reproduced: B at 120%, A changes grain, B reloads at 100%). Fixed both:READER_PREFS(a snapshot reads and writes onlyfs, merged into what is stored) and astoragelistener that keeps every open tab'sfscurrent. Side effect worth having: a snapshot no longer overwrites a live app's grain, lens and collapse set on a shared origin. - 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.
- 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. - 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".
- 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.
- 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).
- Test oracle is the server. The app keeps its state in one closure, so the suite cannot
read
items; it reads/export/jsonafter each edit's save lands and computes the expected readouts itself. That makes each live-update check end to end. - BUILD-NOTES.md is newest first, so the 0.57.0 entry went at the top rather than at the tail.
.adomignoreadded (plus.gitignorelines) to keeptest/node_modules/andtest/out/out of the package. The.adomignoresyntax is assumed to be gitignore-like; check it on the first publish.
Round 1 review fixes
- 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%. - 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.
- 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. - Not-started is shown on the readout, not only on the tooltip; Remaining still counts from today.
- A print-only title line, since the header no longer names the plan in print.
# 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
10. **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%.
11. **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.
12. **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`.
13. **Not-started is shown on the readout**, not only on the tooltip; Remaining still counts
from today.
14. A print-only title line, since the header no longer names the plan in print.