master
Drew Owens Publish 0.60.0 6600ce8

adom-gantt 0.58 to 0.60: a Setup tab, and tags on sections and rows

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.

Built in three checkpoints. Each one is shippable on its own, is published to the wiki when its tests are green, and is reviewed while the next one is being built.

Working copy and tools

  • Source: /home/adom/project/adom-gantt (files-only clone, 0.57.1). The app is gantt.html, served by server.js.
  • Pristine 0.57.1 for diffing: /home/adom/project/adom-gantt-0.57.1-ref/
  • Real plan: /path/to/launch-plan.json (20 sections named like "Launch · Program and gates", "Tech · Laser workcells", "Process · Fab", "Ops · factoryOS"). Never serve that path directly, the server writes sidecars beside its data file: copy it first.
  • Run: node server.js --data <copy>.json --port <8950-8959> --no-auth. Port 8901 is a live preview, leave it alone.
  • Tests: node test/ui_test.mjs is the browser suite (12 minutes for a full run). --only <group> runs a subset; group names are matched by wantAny(...) in the file (editor, live, sections, coverage, header sweep, viewport, text size, ...). Read ~/.claude/skills/app-testing/SKILL.md. The suite has a control-coverage manifest: every new control must be declared, and every safe control is clicked.

Checkpoint 1 (0.58.0): the Setup tab owns section management

  1. A fourth tab in the lens switch, after Analytics, named Setup. It is a page like Table and Analytics are, not a modal. The legend and the chart controls that mean nothing there are hidden, the way Analytics hides them.
  2. Everything the header's Sections button does today moves onto that page: add a section, rename it and set its aside, recolour, reorder, remove (with its rows re-homed the way it works today), all undoable and all in the changelog exactly as now. On a page there is room to show more than the modal did: each section's row count and date range are worth showing.
  3. The Sections button leaves the header. Nothing else in the header moves. The measured header fit (fitHeader) must still fit at every width in all FOUR lenses.
  4. prefs.view and project.view.mode accept the new lens. A refresh on Setup comes back to Setup.
  5. An exported HTML snapshot has the tab too and it works there with no server, like every other edit does in a snapshot.
  6. Tests: the sections flows drive the page instead of the modal; the coverage manifest declares the new controls; the header sweep covers the new lens. The suite stays green.

Checkpoint 2 (0.59.0): tags on sections

  1. Data: phases[].tags, an array of short strings. Trimmed, de-duplicated without regard to case (first spelling wins), a sensible cap on length and count. Absent means none. Tags carry no colour of their own: colour on this chart means the section, and only that.
  2. Persistence end to end in server.js: the sidecar, /export/json, /import/json, the HTML snapshot, and the changelog (a tag edit is an event with a readable line in ?format=md). Old data files and old sidecars load unchanged.
  3. Setup page: edit a section's tags in place (type, Enter, remove with a click; suggest tags already in the plan). A Tags block lists every tag in the plan with how many sections (later, rows) carry it, and can rename a tag everywhere or remove it everywhere. Undo and redo cover all of it, one gesture to one Ctrl+Z.
  4. One click to make tags from name prefixes: for sections named "Launch · Program and gates" it offers the tag "Launch". It adds tags and never renames a section. Only offered when the plan has such names.
  5. Seeing them: the section header row in the chart shows its tags as small quiet chips, and the Table's Phase column can show them on hover or inline, whichever reads better.
  6. Filtering: the legend row gains tag chips. Choosing one or more tags shows only the sections carrying any of them, in the chart and the table alike. It composes with the per-section chips and with Collapse all. It is view state, per person, like the section filter is. With 20 sections and 4 tags the legend must stay tidy: the tags are what make a 20-chip legend manageable, so consider letting a tag chip stand in front of its sections.
  7. Tests for every item above, asserting effects against /export/json.

Checkpoint 3 (0.60.0): tags on tasks and milestones (optional per row)

  1. Data: items[].tags, same rules. A row's EFFECTIVE tags are its own plus its section's; the tag filter uses effective tags, so a task tagged "long-lead" in an untagged section is found by that tag.
  2. Editing: a Tags field in the full item editor and in the New panel, a Tags column in the Table (editable in place, sortable, filterable), and the batch panel can add or remove a tag across the selection.
  3. /export/csv and /import/csv carry a Tags column. Copy and paste carry tags.
  4. Analytics: a per-tag card (items, hours, share), counting effective tags.
  5. The Tags block on Setup counts rows as well as sections.
  6. Tests for every item above.

Housekeeping, every checkpoint

  • Bump the version in package.json and page.json.
  • Update SKILL.md and README.md (the schema tables, the features list, the lens descriptions). No em dashes in prose. Match the surrounding voice.
  • Add an entry at the TOP of docs/BUILD-NOTES.md (it is newest first and about 400KB: read only its head).
  • Do not publish, do not touch /home/adom/project/adom_modules or ~/.claude/skills, leave no processes running. The orchestrator publishes.

Build notes

Build notes - 2026-09-29, checkpoint 1

Built as planned, with these changes to the plan's assumptions:

  • project.view.mode did not accept Analytics either. viewDefaults() read only gantt and table, so "accept the new lens" meant fixing the list, not adding one value. It now reads a single VIEW_LIST (the same list VIEWS is), so any lens, including setup and analytics, can be authored as the opening lens.
  • The modal's class names and row markup were kept (.sec-row, .sec-btn[data-a], .sec-sw, #secNew, #secAdd). The operations were already correct and tested; moving the frame rather than rewriting the rows kept the flows transferable and gives checkpoint 2 one obvious place to add a tag editor per row (a new element in renderSections()), with the Tags block as a second .an-card in .su-stack.
  • One row editor at a time, which the modal did not enforce. On a page with 20 rows, two half-open forms is a real state; opening one closes the other. The swatch toggles.
  • Undo repaints the list. The modal never did (Ctrl+Z with it open left a stale list). The page re-renders from refreshSections() and scheduleSave, skipping while a row editor is open. Checkpoint 2's tag edits should end in refreshSections() too.
  • The date range is read from the rows, not from phases[].start/end, matching the Analytics per-section card and what the chart draws.
  • Coverage now opens the three row editors as states and declares their controls (.sec-nm, .sec-ld, .sec-ok, .sec-chip, .sec-hex, .sec-dest, .sec-go, .sec-no); inside the modal the gate never reached them. #secDone is gone.
  • Observed while building: another process was editing server.js, README.md, SKILL.md and test/server_test.mjs in this same working copy (server-side tags). Checkpoint 1 did not touch server.js; its README and SKILL edits were made as narrow in-place replacements so as not to overwrite that work.

Build notes - 2026-09-30, checkpoint 1 review fixes

  • Section order and row assignment after a reload (V-A) was wrong in both the page and the server's merge, since 0.57.1: the row overrides were checked against the data file's section list before the sidecar's longer list replaced it. Fixed in both (server.js with the coordinator's go-ahead), with a server_test case.
  • An empty section range (V-B) now means "no range of its own" everywhere the page reads one: the axis scan and the start-date shift. The server already skipped it.
  • Sections now have a client-only identity, phaseKey (_k), used by the reorder undo and by every Setup row editor instead of a name or an index. Checkpoint 2's tag editor per section should resolve its section the same way (phaseIndexOfKey) and should end in refreshSections() so an open editor is kept across repaints.
  • Section names must be unique (trimmed, case-insensitive). The checkpoint 2 tag rename should expect that a section name identifies one section.
  • One gesture, one undo, via withOneUndo: checkpoint 2's "rename a tag everywhere" is the obvious next user of it.

Build notes - 2026-09-30, checkpoint 2

Built as planned, with these changes to the plan's assumptions:

  • "The Tags block counts sections (later, rows)" and the legend stand-in were designed together. A tag chip in the legend stands in front of its sections literally: with tags in the plan the section chips fold behind one "N sections" chip (open it to reach them all, remembered as prefs.legendSecs), and choosing tags brings back the chips of exactly the sections on screen. The plan said "consider"; without the fold, tag chips would have made a 20-chip legend a 24-chip one.
  • The tag filter HIDES sections; the section chips still only dim. "Shows only the sections carrying any of them" read as hide, and hiding is what makes the filter worth having on a 20-section plan. It reuses the collapsed-row class and flag, header included, so geometry needed no new code. The section chips keep their dim-in-chart, drop-from-table behaviour inside the filter.
  • The section filter is NOT persisted today (phaseVis is in memory), though the plan said the tag filter should be "like the section filter". The tag filter is persisted (prefs.tagFilter), as the brief asked; the section chips were left as they are.
  • Changelog lines are diffed, not posted. phase.tags comes out of diffAndLog at save time rather than logPhaseEvent, so undo and redo are logged too and a multi-section gesture is one POST. Its via is setup, not the sections the other section edits use.
  • A new undo type, phase.tags, instead of phaseSnapshot, which would also have rolled back the collapse set and the legend visibility.
  • The prefix is the text before the first " · " (spaces both sides). Other separators (" - ", ":") were left out on purpose: "Phase 1 - Design" would have made "Phase 1" a tag.

Notes for checkpoint 3 (rows):

  • getState() already writes items[].tags when a row has an array and added[] carries tags; applyState reads sv.tags. Clearing a row's tags should leave r.tags = [] (not delete the key) so the sidecar says "cleared".
  • phaseShown(i) is the ONE predicate the chart (applyCollapse) and the table (tableRows) use. Effective row tags means a per-ROW predicate; replace the per-lane checks there with one rowShown(r) (own tags plus phaseTagsOf(r)), and decide whether a section header shows when only some of its rows match (probably yes, with those rows).
  • planTags() counts sections; the Tags card and the legend chip counts read it. Add a row count beside it rather than changing what n means, since the fold and the prefix logic rely on it being per section.
  • diffAndLog already has the row side of the server's tags event to fill in (snapSemantic().byId has no tags yet).
  • The client CSV already has the tags column in the server's position.

Build notes - 2026-09-30, checkpoint 2 review fixes

  • The Tags card moved ABOVE the Sections card (it is short, the list is long); the plan said "a Tags block" without a place.
  • A rename carries the reader's tag filter with it, and its undo carries it back: the undo entry for a rename holds the filter as it was.
  • Header chips beat the aside for room; the name comes second, the aside last.
  • The changelog's tag lines are worded by direction (tagged, untagged, retagged).
  • Commit on blur for the tag box. Escape discards.
  • The prefix button still writes one phase.tags line per section; a summary line needs a new server action, left for later.
  • For checkpoint 3: editTags(change, refilter) is the path for any tag gesture that must also move the filter; a row-tag rename in the Tags card should go through it too.

Build notes - 2026-09-30, checkpoint 3

Built as planned, with these changes to the plan's assumptions:

  • The legend chip counts rows now, not sections. With row tags, "# long-lead 0" (no section carries it) or "# Tech 7" (sections) no longer said how much a chip would show. The chip's number is the rows it finds, own or inherited, the same number the Analytics per-tag card calls items. planTags().n still means sections, as checkpoint 2 asked.
  • A section header shows when only some of its rows match (checkpoint 2 said "probably yes"): yes, with those rows and no others. phaseShown(i) became "the header is on screen", and the per-row check is rowShown(r).
  • The batch panel acts on every selected row, milestones included, and at once. Its date fields are task-only and wait for Apply; a tag means the same on a milestone and there was no reason to make an add wait on a date Apply.
  • The item editor logs via: item-editor, not editor. Every other field that panel saves has always been logged as item-editor; editor is the bar panel's. SKILL.md's example line was corrected rather than the via.
  • A row created with tags writes its own tags line (via: create) after its add, so the log says where the tag came from. A paste keeps its single summary line.
  • The Tags column shows the section's tags too, dashed, after the row's own. It is the one place the effective-tag rule the filter uses can be seen row by row. Only the row's own are edited there.
  • Every save re-checks the plan's tag picture (a signature of planTags()), because deletes, undos of creates and section moves change which tags exist and what a chip would show without being tag edits.
  • Not done: section tags are not editable from the table (they belong to Setup); the chart shows only a row's OWN tags in its label (the header carries the section's).

Build notes - 2026-09-30, checkpoint 3 review fixes

  • Row tags in the chart label now take only the room the name and pills leave (flex: 1 1 0, the gap inside their box), so the name is never cut for them.
  • ; is not allowed inside a tag (it is the CSV separator); both normalisers turn it into a space. The CSV is documented as lossy for section and inherited tags.
  • The table's Tags cell saves per chip, like the Setup box, and keeps its caret in view.
  • Lane header counts read "2 of 3" under a tag filter; a delete or tag edit that prunes the filter restores it on Ctrl+Z (viewFilter on the undo entry, kept by time travel).