← Commit history

0.33.0: client applyState now rebuilds the section list from the sidecar (mirrors server mergedData), so section rename/recolour/reorder/create/delete survive a reload; client lanes + item placement now equal /export/json. Fixes the DC plan scramble; reconciled + compacted live. describeEvent gains plan.reconcile.

Drew Owens ·f99882e4d5 · ·parent a6c604c
5 files changed +90−4
docs/BUILD-NOTES.md+44
@@ -3,6 +3,50 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-17 (0.33.0, the client persists the section list on load)++**The bug.** The client's `applyState` never applied the sidecar's full section+list. It only patched each phase's start/end by index (`state.phases.forEach(sv =>+phases[sv.i]…)`), so a section rename, recolour, reorder, create or delete — all of+which the section editor writes into the sidecar as a full, named `phases` list —+was silently dropped on reload. The chart was rebuilt from the DATA FILE's phases+every time. The server's `mergedData()` had always done the right thing (a sidecar+carrying a named phase list OWNS the sections), so `/export/json` and the client+disagreed. On the DC plan this was stark: Drew had reorganised a "Workcells"+catch-all into four topical lanes and deleted Workcells via the section editor. The+sidecar held the correct six lanes with every item's lane override; the stale data+file still held seven. The client rendered the data file's seven names but applied+the sidecar's 0–5 item indices against them — every item in the wrong lane, a+phantom empty lane, Workcells still showing.++**The fix mirrors the server exactly.** `applyState` now branches the same way+`mergedData()` does: a sidecar whose `phases` entries carry a `name` OWNS the list,+so `phases[]` is rebuilt in place (same field set — name, uid, lead, color, start,+end) and the derived headers, the legend, the row grouping and the lane colours are+re-derived from it (`buildDerivedHeaders` → `placeHeaders` → `reflowRows` →+`refreshSections` → `applyFilter`, the same tail every section mutation and+`restorePhaseState` already run). `phaseVis` is resized to the new lane count. An+older, date-only sidecar (`{i,start,end}` by index, no name) still applies against+the data file's phases exactly as before, so the example files and any+pre-editable-sections sidecar load unchanged; a sidecar with no `phases` key at all+hits the same no-op path. After the fix the client's lane list and every item's lane+equal `/export/json`, and a rename/recolour/reorder/create/delete of a lane survives+a reload — the regression the bug had been hiding.++**`refreshSections` in the tail was not optional.** `buildDerivedHeaders` reuses a+header's DOM by phase uid and only updates the header OBJECT's label; the visible+`.lane-name` text and the lead span are written by `refreshHeaderRow`. Without+`refreshSections` a reloaded rename showed the new name in the legend (rebuilt from+scratch) but the OLD name in the chart header. Caught by the reload test.++**Then the plan was made canonical.** With the client fixed, the latent data-file/+sidecar divergence was compacted away on the live plan: `/compact` wrote+`mergedData()` (the correct six-lane state) as the data file and emptied the sidecar,+after a `plan.reconcile` changelog event (actor Drew Owens) recording that Workcells+was intentionally deleted and every item and its resolved lane are intact. Proven+nothing-lost: identical uid set and identical resolved lane-name per item before and+after, 160 items, 6 lanes. `describeEvent` gained a sentence for `plan.reconcile`.+ ## Build notes, 2026-08-17 (0.32.0, the status pill is editable in the chart)  The little status pill next to a task name in the label column ("not started",
gantt.html+40−1
@@ -2038,7 +2038,37 @@ function applyState(state){     if(sv.name)r.name=sv.name;     if(typeof sv.phase==='number'&&phases[sv.phase])r.phase=sv.phase;   });-  (state.phases||[]).forEach(sv=>{ const p=phases[sv.i]; if(p){ if(sv.start)p.start=sv.start; if(sv.end)p.end=sv.end; } });+  // The sidecar's phase list, applied EXACTLY as the server's mergedData does, so+  // the client renders what /export/json says. Two shapes. A sidecar written since+  // sections became editable carries the FULL list, every entry named, and it OWNS+  // the section list: it is rebuilt in place so a rename, recolour, reorder, create+  // or delete made in the section editor survives a reload (the render at the end of+  // this function re-derives the headers, the legend and the row grouping from it).+  // An older sidecar (v3/v4 date-only) carries just {i,start,end} patches by index+  // and is applied against the data-file phases, exactly as before — the example+  // files and any pre-editable-sections sidecar keep loading unchanged.+  const sph=state.phases;+  let phasesRebuilt=false;+  if(Array.isArray(sph)&&sph.length&&sph.some(p=>p&&p.name!==undefined)){+    const rebuilt=sph.map(p=>{+      const o={name:p.name,start:p.start||'',end:p.end||''};+      if(p.uid)o.uid=p.uid;+      if(p.lead)o.lead=p.lead;+      if(p.color)o.color=p.color;+      return o;+    });+    phases.length=0; phases.push(...rebuilt);+    // phaseVis is one flag per lane; resize it to the rebuilt list, all visible.+    phaseVis.length=0; phases.forEach(()=>phaseVis.push(true));+    // Relabel/rehome the derived headers to the rebuilt list and drop any whose phase+    // is gone — that DOM removal is what clears the phantom empty lane. Safe here:+    // the chart was built in init() (rowsBuilt), and buildDerivedHeaders reuses a+    // header's DOM by phase uid, so nothing already on screen is orphaned by mistake.+    if(rowsBuilt)buildDerivedHeaders();+    phasesRebuilt=true;+  } else {+    (sph||[]).forEach(sv=>{ const p=phases[sv.i]; if(p){ if(sv.start)p.start=sv.start; if(sv.end)p.end=sv.end; } });+  }   // Before the order, always: the saved order was written after these rows were   // removed, so applying it first would compare against the wrong length and be   // rejected by the anti-shrink guard.@@ -2050,6 +2080,15 @@ function applyState(state){   // saved order outright and silently leave the plan in data-file order.   const rowCount=items.filter(r=>!isDerivedGroup(r)).length;   if(Array.isArray(state.order)&&state.order.length===rowCount)applyOrder(state.order);+  // A rebuilt phase list changed the sections, their names, leads, order and colours,+  // so re-derive both lenses from phases[], the same tail every section mutation and+  // restorePhaseState run: placeHeaders/reflowRows regroup and recolour the rows (the+  // saved order above may already have, which is idempotent), and refreshSections+  // rewrites each header's name and lead in the DOM (buildDerivedHeaders only reused+  // the header object, not its text) and rebuilds the legend and the table. applyFilter+  // restores lane visibility. Geometry (axis, caps, deps) is left to startApp, which+  // rebuilds the timeline right after this returns.+  if(phasesRebuilt&&rowsBuilt){ placeHeaders(); reflowRows(); refreshSections(); applyFilter(); } } // ── Changelog ── //
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.32.0",+  "version": "0.33.0",   "description": "Interactive Gantt chart viewer for project timelines, with a table view 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. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",   "main": "server.js",   "bin": {
page.json+2−2
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.32.0",+  "version": "0.33.0",   "description": "Interactive Gantt chart viewer for project timelines, with a table view 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. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",   "main": "server.js",   "bin": {@@ -63,4 +63,4 @@     "name": "Drew Owens",     "email": "[email protected]"   }-}\ No newline at end of file+}
server.js+3
@@ -463,6 +463,9 @@ function describeEvent(e) {   if (action === 'compact') {     return `${who} FLATTENED live edits into the data file: ${e.items || 0} item(s) are now the base and the sidecar is empty. Events above this line describe the plan before the flatten${via}`;   }+  if (action === 'plan.reconcile') {+    return `${who} RECONCILED the plan: the section reorganisation is now the data file itself — ${e.phases || 0} lane(s) over ${e.items || 0} item(s). The "Workcells" catch-all was intentionally deleted and its items distributed into their topical lanes; every item and its resolved lane are intact. The client now rebuilds the section list from the sidecar on load, so a rename, recolour, reorder or delete of a lane can no longer drift between what is saved and what is shown${via}`;+  }   if (action === 'lane.remove') {     const kept = Array.isArray(e.rehomed) && e.rehomed.length ? ` Kept and re-homed to ${e.into || 'another lane'}: ${e.rehomed.join('; ')}.` : '';     const gone = Array.isArray(e.removed) && e.removed.length ? ` Removed: ${e.removed.join('; ')}.` : ' Nothing else was in it.';