app
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
← Commit history
0.44.0: Alt+Arrow opens a collapsed lane the row lands in, and groups a gapped selection into a block before moving it
6 files changed
+225−6
README.md+1−1@@ -239,7 +239,7 @@ long editing run. | Delete | Delete the selected row, undoable. Only when a row is selected and no field is open, so Delete inside an editor just edits text. Backspace is deliberately not bound | | Shift+D | Dependency mode: click the task that comes first, then the one that waits on it. The armed first task wears the normal selection outline. Clicking a pair that already has a dependency removes it. Click an arrow to pick it, then Delete twice to remove that one. Stays on so you can chain them. Esc lets go of a picked arrow, then the pick, then leaves | | Shift+N | Open the New panel. Fires only when nothing else is going on, so a capital N typed into a field stays a capital N |-| Alt+Up / Alt+Down | Move a row one slot: the focused row in the table, the selected task in the chart. Crossing a lane boundary moves the row into that lane. Under a column sort the row stays inside the run of rows the sort considers tied with it. The pane follows the row it is moving, keeping five rows visible ahead of it in the direction of travel, and both columns stay locked |+| Alt+Up / Alt+Down | Move a row one slot: the focused row in the table, the selected task in the chart. Crossing a lane boundary moves the row into that lane, and a lane that is collapsed OPENS the moment the moving row lands in it, so you can see what you are moving among. Under a column sort the row stays inside the run of rows the sort considers tied with it. The pane follows the row it is moving, keeping five rows visible ahead of it in the direction of travel, and both columns stay locked. With several rows selected the whole set moves as a group; if the selection has gaps in it, the FIRST press closes it up into one block at the topmost selected row and every press after that moves the block | | Tab / Shift+Tab | In the table, walk the selected row's fields. With no editor open it opens the first editable cell (Shift+Tab the last); with one open it commits and moves to the next. Wraps across rows and around the ends, and follows the visible order under a sort. With no row selected, Tab walks the page's own controls as usual. In the edit panels it is one keypress per field the same way, including the date fields, which Tab leaves whole instead of walking their day, month and year segments | | Esc | Cancel the open field, then clear the row focus, then clear the selection | | Ctrl+click | Toggle item in selection |
SKILL.md+2−2@@ -230,7 +230,7 @@ rather than a category. - **Saving is whole-state and last-write-wins.** Two people editing at once WILL overwrite each other. Each save carries the revision it was based on and a stale save is refused with 409, at which point that tab stops autosaving and offers a reload rather than clobbering the other person - **Dependency mode** (the Deps button with the chart controls, or **Shift+D**): click the task that comes FIRST, then the task that waits on it, and the dependency is drawn. The armed first task wears the normal selection outline so you can see what the pair is waiting on. The pair resets and the mode stays on, so a run of dependencies is one sitting. Clicking a pair that ALREADY has a dependency REMOVES it, which is the same removal as the editor's ×: one undo entry, one changelog event. Refuses anything that would make a cycle, naming the shortest loop path. Milestones are not endpoints: a dep is stored as a task id and a milestone in the data file has none. **Clicking an arrow picks it**, in this mode only, and **Delete** twice removes the dependency it stands for (the first press arms and says so, the second commits; any other key, a click or Esc withdraws the question). Outside the mode the arrows take no clicks at all. Esc lets go of a picked arrow, then a half-made pick, then leaves the mode. While the mode is on, bar clicks mean dependencies: no editor, no drag, no reorder. Ctrl+Z undoes the last add or removal without leaving the mode - **Copy IDs**: with rows selected, the table's tools row and the chart's selection chip (and the batch panel) copy the selected items' stable ids, one per line as `id<TAB>name`, for pasting into an AI chat to request batch edits. Ctrl+C does the same when nothing is focused and no text is selected. If the clipboard is refused the ids appear in a selectable box rather than failing silently-- **Row multi-select in the table**: ctrl-click toggles a row, shift-click takes the range from the anchor following the VISIBLE order under sorts and filters. It is the SAME selection as the chart's, so rows picked in one lens are selected in the other. Modified clicks never open a cell editor and never start a native text selection; unmodified click-drag still selects cell text. Alt+Up/Down moves the whole set (plan order only; refused under a sort), and Delete removes the whole set as one undo+- **Row multi-select in the table**: ctrl-click toggles a row, shift-click takes the range from the anchor following the VISIBLE order under sorts and filters. It is the SAME selection as the chart's, so rows picked in one lens are selected in the other. Modified clicks never open a cell editor and never start a native text selection; unmodified click-drag still selects cell text. Alt+Up/Down moves the whole set (plan order only; refused under a sort), and Delete removes the whole set as one undo. A selection with GAPS in it groups itself up on the first Alt+Arrow press rather than moving: the selected rows close up into one contiguous block at the topmost one's slot - **Arrows steer clear of the bars.** A dependency leaves the source bar's END sideways, drops, and arrives at the next bar's START sideways again, with rounded elbows, so which end is the cause is always readable. It routes AROUND the bars in between rather than through them: the drop moves to the nearest clear channel, and a long or backward route runs along the empty lane between two rows rather than across a task that has nothing to do with it. Where a bar lies clean across the only gap, the arrow goes round the end of it. Nothing to configure; it is re-decided on every redraw, including live during a drag - **Deps column** in the table: a narrow read-only indicator, needs and blocks counts with the names on hover, blank when there are none. Clicking it opens the editor where dependencies are changed. Sortable by total count - **Ctrl+Z** to undo (100-level stack)@@ -238,7 +238,7 @@ rather than a category. - **Up / Down** in the table move the selected row, following the visible order under any sort or filter. They never scroll the pane on their own: the browser's default is suppressed and the pane moves only enough to keep the selected row in view. The first press selects the first visible row. In the chart the arrows are left alone - **Enter** on the selected row opens its NAME editor, ready to type with the text selected. Inside a cell editor Enter commits and steps down one row, the spreadsheet convention, so a column of values is Enter, type, Enter, type; it does not open the next editor, and a second Enter does - **Shift+N** opens the New panel, gated on the app being idle: no editor, panel, menu or drag in progress, and nothing focused that a person could be typing into-- **Alt+Up / Alt+Down** moves one row one slot, and the pane FOLLOWS the row it is moving, keeping five rows visible ahead of it in the direction of travel and scrolling only when it has to, in both lenses and through a held-key repeat. Both chart columns stay locked, and the usable top of the pane is measured under the sticky axis and the pinned section header rather than at the pane's edge. It moves the WHOLE chart selection as a group when more than one row is selected, following the leading row: each selected row steps one place so the gaps between them survive, and if any of them has nowhere to go the press does nothing rather than squashing the selection against the end. One undo per press either way. The table moves its focused row (the row whose cell editor was touched last, marked with an accent bar on its left edge); the chart moves its selected task. In plan order the movement is free, and stepping over a group header re-homes the row into that lane, phase, bar colour and dashed rule together, exactly as a drag does. Under a column sort the movement is bounded to the contiguous run of rows the sort considers TIED with this one, so an owner-sorted Adonis task moves within the Adonis block and nowhere else: the visible sort stays truthful and what changes is the tie-break, which is plan position, which persists. A row alone in its sort key does not move and says so. Undo is per press; the changelog coalesces a burst into one line+- **Alt+Up / Alt+Down** moves one row one slot, and the pane FOLLOWS the row it is moving, keeping five rows visible ahead of it in the direction of travel and scrolling only when it has to, in both lenses and through a held-key repeat. Both chart columns stay locked, and the usable top of the pane is measured under the sticky axis and the pinned section header rather than at the pane's edge. It moves the WHOLE chart selection as a group when more than one row is selected, following the leading row: each selected row steps one place so the gaps between them survive, and if any of them has nowhere to go the press does nothing rather than squashing the selection against the end. One undo per press either way. The table moves its focused row (the row whose cell editor was touched last, marked with an accent bar on its left edge); the chart moves its selected task. In plan order the movement is free, and stepping over a group header re-homes the row into that lane, phase, bar colour and dashed rule together, exactly as a drag does. Under a column sort the movement is bounded to the contiguous run of rows the sort considers TIED with this one, so an owner-sorted Adonis task moves within the Adonis block and nowhere else: the visible sort stays truthful and what changes is the tie-break, which is plan position, which persists. A row alone in its sort key does not move and says so. **A COLLAPSED lane OPENS the moment the moving row lands in it**, so the row never vanishes mid-press and the reader can see the rows they are now moving among; it opens on the crossing keystroke and not before, which is a different question from the five-row scroll buffer, and a lane merely stepped over (an empty one) stays shut. That is a view state, so undoing the move puts the row back and leaves the lane open. **A selection with GAPS in it GROUPS itself up on the first press**: the selected rows close up into one contiguous block at the TOPMOST selected row's slot, in the order they already had, with the unselected rows between them closing up below, and a toast says so. That press does not also step the block, because a block anchored on the topmost row and then moved one place is no longer aligned to the thing it was anchored to; the selection is contiguous afterwards, so every press after it moves the block as one. Re-scatter the selection with another Ctrl+Click and the next press groups it again. A selection that is already contiguous is untouched and moves on the first press exactly as before. Undo is per press; the changelog coalesces a burst into one line - **Tab / Shift+Tab** walks the selected row's fields at one keypress per column, including the select and date columns, and does the same thing field by field in every edit panel (task, milestone, item, New and batch): one Tab leaves a date field for the next field rather than stepping through its segments, in both directions, while the arrows and typing still edit the segments normally. A select cell shows its choices in the app's own list rather than the browser's popup, because that popup swallows the first Tab to close itself; the select keeps all its native keyboard behaviour. Date cells keep their native arrow keys for segments and values, and Tab leaves the field instead of walking its segments. With no editor open it opens the first editable cell (Shift+Tab the last); inside an editor it commits the field and opens the next editable cell, with the same ready-to-type conventions as a click (a select opens its list, short text is selected, notes get the caret at the end). It skips read-only cells and the delete button, follows the visible order under a sort, and wraps across rows and around both ends of the table - **Auto-save** to `<datafile>-state.json` sidecar (800ms debounce, flushed immediately if the page is closed or hidden, so the changelog and the sidecar cannot disagree) - **Auto-load** saved state on page refresh
docs/BUILD-NOTES.md+121@@ -3,6 +3,127 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-19 (0.44.0, Alt+Arrow: shut lanes open, scattered selections group)++Two changes from Drew, both about moving rows with Alt+Up and Alt+Down. `gantt.html`+only. `server.js` did not move: nothing about the sidecar, the changelog or the save+protocol changed, and the one new changelog line rides the `reorder` event and the+`key-move` via that already existed. **No live server was restarted and no live plan was+opened at any point.** The suites run on synthetic fixtures on their own port.++### 1. A collapsed lane OPENS when the moving row lands in it++A collapsed lane hides its rows. Move a row into one and it used to vanish in the middle+of a press, and every press after that was a guess at where it had got to: on the chart+the row kept its stale visibility until the next `applyCollapse`, in the table it dropped+straight out of the list, and a group move called `applyCollapse` and so lost the rows+outright.++`openLandingLanes(rows)` runs at the end of all three keyboard-move paths (single row,+group move, and the new grouping press). It asks one question of each moved row: is the+lane you are now IN collapsed? If so it opens it, writes `prefs.collapsed`, and calls+`applyCollapse`.++**The trigger is landing, not proximity, and that distinction is the feature.** 0.42.0's+scroll buffer keeps five rows visible ahead of the moving row, so a row walking towards a+shut lane is inside the buffer's reach for five presses before it gets there. None of+those presses opens anything. The suite asserts it directly: five presses down the last+five rows of Alpha, `[true,true,true,true,true]` for "Bravo still shut", and the sixth+press, the one that crosses, opens it.++**A lane merely stepped over stays shut.** One press at a boundary lands the row at the+first (or last) slot of the next lane that HAS rows; an empty lane in between is stepped+over because it has no slot to land in. Nothing landed there, so nothing opens. Echo, the+empty lane in the bench, is asserted still shut after a row has crossed it into Delta.++**The expansion is a real one and it persists.** Same state the header toggle writes, same+`prefs.collapsed`, so it survives the keys coming up and survives a reload. **Undo does+NOT re-collapse it.** Undo owns the plan; what the reader is looking at is not the plan.+Asserted both ways: one Ctrl+Z restores the exact row order and the lane stays open.++The buffer keeps working through the expansion because the expansion happens BEFORE the+reflow the follow measures against. The suite walks a row into a shut 24-row lane and+checks, on every press including the one that adds all 24 rows to the pane in a single+frame, that the row is inside the pane, five rows are clear ahead of it, and the two+columns are still scroll-locked.++### 2. A gapped selection GROUPS itself up before it moves++Ctrl+Click builds selections with gaps in them. Drew's screenshot is four rows chosen out+of six consecutive ones, with unselected rows sitting between them. 0.15.0's group move+steps each selected row one place, which deliberately PRESERVES those gaps. That is right+for a set the reader wants spread out and wrong for what Drew actually wants, which is to+bring them together.++So the first Alt+Arrow press on a gapped selection closes it up: every selected row moves+to the topmost selected row's slot, in the order it already had, and the rows that were+between them close up below the block.++**The first press only groups. It does not also step the block.** This was the one real+design question, and three things decided it:++- **The anchor stops meaning anything.** The whole point is a block aligned to the topmost+ selected task. Group and then step down in the same press and the block is anchored one+ row BELOW the thing it was aligned to, which is not what was asked for.+- **Up and down would have to mean different things.** Alt+Down would group-then-step-down+ and Alt+Up would group-then-step-up, so the same selection would form a block in two+ different places depending on which arrow started it. Grouping alone is direction-free+ and lands in exactly one place either way.+- **One press, one Ctrl+Z.** Two conceptually different changes under one undo entry is a+ Ctrl+Z that does more than the reader asked it to.++A toast says so at the moment it happens: *"Grouped 4 tasks, the arrows now move the+block"*. The block is contiguous afterwards, so the NEXT press moves it through the+existing group-move code with no new branch, which is why a held burst behaves: the first+press of the burst groups, the other eleven move.++**No flag is kept, and none is wanted.** The SHAPE of the selection is the state:+`gappedSelection(rows)` measures the selected rows' positions among the real rows (a+section header between two of them is not a gap, because it is not a slot anything can+occupy) and returns whether they span more slots than there are of them. Scatter the+selection again with another Ctrl+Click and the next press groups it again; close it up by+hand and the next press moves it. The suite drives exactly that round trip.++**It is one real reorder.** One `{type:'order'}` undo entry carrying every phase it+changed, which is the same shape a group move records, so redo comes free from 0.20.0's+inverse machinery. One changelog batch: the debounced `key-move` save produces a single+`reorder` event, `via:"key-move"`, `hint:"4 rows grouped together"`. Asserted by count+against the server's own changelog.++**Phase membership follows the existing rules and nothing new.** The block forms at the+topmost row's slot, so rows pulled up under that row's header join that lane and recolour,+by exactly the `phaseAtPosition` rule a single-row move into that slot would have followed.+Asserted on colour, not on internal state: the two Delta rows grouped under `c4` come out+the same computed `background-color` as `c4` itself, and one undo puts both the lane and+the colour back.++**A contiguous selection is untouched.** No grouping, no toast, no extra event: the first+press moves it exactly as 0.43.0 did.++### The bug this round nearly shipped++`items.length=0; items.push(...coalesceOrder(rows));` reads `items` AFTER emptying it,+because those are two statements and the second one's argument is evaluated last. The+result was an empty `items`, which `reflowRows` cannot detect (it only appends, it never+removes), so the DOM kept its old order and the toast still fired: a press that claimed to+have grouped four rows and had in fact deleted the whole plan from memory. Caught by the+first probe rather than by reading, which is the argument for driving the browser before+writing the assertions. The fix is the idiom `moveSelectedRows` already used: compute the+next order first, then replace.++### Verification++`v44a` (54 assertions, the shut lane), `v44b` (48, the scattered selection) and `v44c`+(19, both of them through the table lens) on a five-lane bench with a collapsed middle+lane, an EMPTY lane and a 24-row lane. Expectations for the ordering cases come from+`coalesceRef` and `moveBlockRef`, a second implementation written from the spec, so a+wrong answer has to be wrong in two places to pass. Zero JS errors.++Two A/B runs against 0.43.0, each build on its own port with its own copy of the fixture:+the `mssel` and `msphase` legacy suites (byte identical), and a transcript of everything+Alt+Arrow does that this round was NOT asked to change, contiguous multi-select included+(byte identical).+ ## Build notes, 2026-08-19 (0.43.0, finish-to-start means back to back) One line from Drew: "auto schedule is leaving a one-day gap between tasks w/ deps, please
gantt.html+98@@ -1847,6 +1847,29 @@ function phaseAtPosition(r){ for(let k=i-1;k>=0;k--) if(items[k].type==='group') return items[k].phase; return r.phase; }+// A collapsed lane hides its rows, so a row moved into one used to vanish in the+// middle of a press and every press after that was a guess at where it had got to.+// The lane OPENS at the moment the moving row actually crosses into it.+//+// That is a different question from the five-row scroll buffer, and deliberately so:+// the buffer is about what is near the EDGE OF THE PANE, this is about where the row+// has LANDED. A row travelling towards a collapsed lane opens nothing, however close+// it gets; a row that enters one opens it, on that keystroke and no earlier. A lane+// merely stepped over on the way (an empty one, which a single press does skip)+// stays shut, because nothing landed in it.+//+// A real expansion, the same state the header toggle writes and stored in prefs+// beside it, so it survives the keys coming up. Undoing the move does NOT shut it+// again: what the reader is looking at is not part of the plan, and the undo of a+// move owns the plan.+function openLandingLanes(rows){+ let opened=false;+ rows.forEach(r=>{ if(r&&isCollapsed(r.phase)){ collapsed.delete(r.phase); opened=true; } });+ if(!opened)return false;+ prefs.collapsed=[...collapsed]; savePrefs();+ applyCollapse();+ return true;+} function moveItemAdjacent(r,target,after){ const ri=items.indexOf(r); if(ri<0)return false; items.splice(ri,1);@@ -1933,9 +1956,82 @@ function planGroupMove(rows,dir){ } return arr; }+// ── Grouping a scattered selection ──+//+// Ctrl+Click builds a selection with GAPS in it: rows taken from up and down the+// plan with unselected rows left between them. Stepping that a slot at a time keeps+// the gaps, which is right for a set the reader wants to stay spread out and wrong+// for the far commoner intent, which is to bring them together. So the FIRST+// Alt+Arrow press on a gapped selection GROUPS it: the selected rows close up into+// one contiguous block at the topmost selected row's slot, in the order they already+// had, and the rows that were between them close up below it.+//+// That press does not also step the block. Anchoring the block on the topmost+// selected row and then moving it one place in the same press would leave it+// anchored one row off the thing it was aligned to, would make Alt+Up mean something+// different from Alt+Down at the moment the block forms, and would put two changes+// under one Ctrl+Z. One press, one visible effect, one undo. The block is contiguous+// afterwards, so the NEXT press moves it as one, which is exactly what a contiguous+// selection has always done, through exactly the same code.+//+// No flag is kept and none is wanted: the SHAPE of the selection is the state. Scatter+// it again with another Ctrl+Click and the next press groups it again, close it up by+// hand and the next press moves it.+function gappedSelection(rows){+ // Measured over real rows only. A section header between two selected rows is not+ // a gap: it is not a slot anything can occupy, and a selection either side of one+ // is contiguous and has always moved as a block.+ const real=items.filter(r=>r.type!=='group');+ const pos=rows.map(r=>real.indexOf(r)).sort((a,b)=>a-b);+ if(pos.length<2||pos[0]<0)return false;+ return pos[pos.length-1]-pos[0]!==pos.length-1;+}+// The selected rows go to the topmost one's slot, in plan order; everything else+// keeps the order it had and simply closes up. Headers stay where they are relative+// to the rows still around them, so the lane each row LANDS in is read back off its+// new position afterwards, by the same phaseAtPosition every other keyboard move+// asks. Rows pulled up under the topmost row's header join that lane and recolour.+function coalesceOrder(rows){+ const set=new Set(rows);+ const out=[]; let placed=false;+ items.forEach(r=>{+ if(!set.has(r)){ out.push(r); return; }+ if(placed)return;+ placed=true; rows.forEach(x=>out.push(x));+ });+ return out;+}+function groupSelectedRows(rows,dir){+ const before=items.map(rowOrderKey);+ const fromPhases=rows.map(r=>({r,from:r.phase}));+ // Computed BEFORE items is emptied, the way a group move computes its own next+ // order first: read the plan, then replace it.+ const next=coalesceOrder(rows);+ items.length=0; items.push(...next);+ // One undo entry and one inverse, the same shape a group move records, so one+ // Ctrl+Z puts every row back exactly where it was and Ctrl+Shift+Z regroups them.+ pushUndo({type:'order',order:before,phases:fromPhases});+ rows.forEach(r=>{+ const np=phaseAtPosition(r);+ if(np!==undefined&&np!==r.phase){+ r.phase=np;+ if(r.id&&barEls[r.id])barEls[r.id].bar.style.background=phaseColor(np);+ if(r._msLine)r._msLine.style.color=phaseColor(np);+ }+ });+ openLandingLanes(rows);+ applyCollapse();reflowRows();applyFilter();refreshChartGeometry();renderTable();+ refreshSelVisuals();+ followMovedRows(rows,dir);+ moveHint=rows.length+' rows grouped together';+ flash(saveInd,'Grouped '+rows.length+' tasks, the arrows now move the block',3200);+ scheduleKeyMoveSave();+ return true;+} function moveSelectedRows(dir){ const rows=selectedRows(); if(rows.length<2)return false;+ if(gappedSelection(rows))return groupSelectedRows(rows,dir); const before=items.map(rowOrderKey); const fromPhases=rows.map(r=>({r,from:r.phase})); const next=planGroupMove(rows,dir);@@ -1952,6 +2048,7 @@ function moveSelectedRows(dir){ if(r._msLine)r._msLine.style.color=phaseColor(np); } });+ openLandingLanes(rows); // a block that has entered a shut lane opens it applyCollapse();reflowRows();applyFilter();refreshChartGeometry();renderTable(); refreshSelVisuals(); // the selection survives, and still looks it followMovedRows(rows,dir);@@ -2076,6 +2173,7 @@ function moveFocusedRow(dir){ if(r.id&&barEls[r.id])barEls[r.id].bar.style.background=phaseColor(np); if(r._msLine)r._msLine.style.color=phaseColor(np); }+ openLandingLanes([r]); // it landed in a shut lane, so the lane opens reflowRows();applyFilter();refreshChartGeometry();renderTable(); // A milestone's row key is its position among the milestones, so it can change // under the row that carries it. Re-read it rather than keep a stale one.
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.43.0",+ "version": "0.44.0", "description": "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 business days, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for hours per person, status, per-section totals, milestones, dependency stats and what is overdue. 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.43.0",+ "version": "0.44.0", "description": "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 business days, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for hours per person, status, per-section totals, milestones, dependency stats and what is overdue. 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": {@@ -81,4 +81,4 @@ "name": "Drew Owens", "email": "[email protected]" }-}+}