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.49.0: the critical path is the LONGEST path, global or one per section
Redefines Critical. It was zero-float CPM, which anchors on the plan's latest
finish and therefore always answered 'which chain ends last'; a short simple
chain running into December beat a ten-row interconnected build finishing in
November. It now finds the maximum-weight path in the dependency DAG, where a
task weighs its duration in working days (weekends and federal holidays out)
and a milestone weighs nothing.
One button, three states, cycled the way the milestone style is: off, then
Critical: plan (one path across the whole plan), then Critical: sections (one
longest path inside every section at once, each computed on that section's own
subgraph so a dependency that crosses sections is not an edge there). Ties break
deterministically: working days, then calendar span, then node count, then plan
order. The mode is a saved preference. The glow, Auto-schedule and server.js are
unchanged.
6 files changed
+480−86
README.md+4−2@@ -6,7 +6,9 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau **Auto-schedule the whole plan.** One press of **Auto-schedule** lays every task out BACK TO BACK with the work it waits on, and so that nothing starts or ends on a Saturday, a Sunday or a US federal holiday. A successor starts ON its latest dependency's end, so the two bars meet on the chart with no column of white between them. Tasks move earlier as well as later: slack is pulled in and a conflict is pushed out. A task with no dependencies is an anchor and keeps its start, since that is the one thing the plan cannot derive. Durations are preserved in WORKING days, so a five-day task laid across a weekend, or across Labor Day, still takes five working days: a predecessor that ends on the Friday hands off on the Friday, and the weekend sits inside the successor's span rather than between the two of them. Milestones move off a weekend or a holiday to the next working day and sit on the day their last dependency ends. The dependency graph crosses sections freely and so does the schedule. A cycle is left exactly as it is, named in the toast, because there is no honest answer for a row that waits on itself. It is one Ctrl+Z for the whole pass and one line in the changelog, and pressing it on an already aligned plan reports "0 tasks moved" and writes nothing. -**Critical path.** **Critical** lights up the chain of work with no slack in it, the one that decides the finish date: the bars, the dependency arrows along it and any milestone on it glow white and breathe, in a saturated indigo on paper. Classic CPM over working days, weekends and federal holidays excluded, on the dates the plan has right now, so it is independent of Auto-schedule and answers the question about the plan as it stands. It uses the same finish-to-start lag Auto-schedule lays the plan out with, so the two buttons can never disagree: align the plan and the chain it aligned reads as zero float. Everything else stays exactly as it was, selection and dependency mode included, and the glow recomputes as you edit.+**Critical path, in two modes.** **Critical** lights up the LONGEST run of work in the plan, start to end: the bars, the dependency arrows along it and any milestone on it glow white and breathe, in a saturated indigo on paper. The dependency graph is read as a DAG, a task weighs its duration in WORKING days (weekends and US federal holidays excluded) and a milestone weighs nothing, and the critical path is the maximum-weight path through it. So it is the hardest run of work through the plan and not merely whatever chain happens to end last. Nothing is rescheduled: it reads the dates the plan has right now, independently of Auto-schedule.++One button, three states, cycled the way the milestone style is: **off**, then **Critical: plan**, the single longest path across the whole plan, then **Critical: sections**, one longest path inside EVERY section at once. In per-section mode each lane is answered on its own subgraph, so a dependency that crosses sections is not an edge and never lights up, and a section with no dependencies among its own rows lights its longest single row. Ties are broken deterministically: more working days, then a greater calendar span, then more rows, then whichever starts earlier in the plan. Everything else stays exactly as it was, selection and dependency mode included, the glow recomputes as you edit, and the mode is remembered across reloads. **Analytics.** A third lens, next to Gantt and Table. Total effort for the plan in hours (working days times eight, a forty-hour week), the working hours LEFT between today and the project end, the AVERAGE hours per assigned person, then one row per person carrying their hours, share and item counts alongside their four status counts, the unassigned bucket included. Status counts overall, per-section totals with the range each section actually covers, milestones with days from today, dependency stats with a cycle warning, and everything overdue. Read-only, whole-plan, and re-derived from the same rows every time you switch to it. @@ -263,7 +265,7 @@ table, the item editor or the batch editor are taken exactly as typed. | Gantt / Table / Analytics | Switch lens. Pinned to the left of the header so it never moves | | Fit | Fit the whole plan into the pane, no horizontal scrolling | | Auto-schedule | Align every task to its dependencies, finish to start, skipping weekends and US federal holidays. One undo. Disabled when the plan has no dependencies |-| Critical | Light up the chain with no slack in it. A toggle, and it recomputes as the plan changes |+| Critical | Light up the longest run of work. Cycles off / whole plan / one per section, and it recomputes as the plan changes | | A- / A+ | Step the text size for both views. Click the percentage to reset | | Hover "today" | The current date | | Click a table cell | Edit owner, start, deadline, status or notes in place |
SKILL.md+4−3@@ -8,7 +8,8 @@ description: > auto-save to a sidecar state file. One button auto-schedules the whole plan so every dependency lines up finish-to-start on working days with no weekend or federal-holiday starts or ends, and another lights up the- critical path.+ critical path, meaning the longest run of work in the plan, either once+ across the whole plan or once inside every section. A Table view is the second lens over the same data: every task and milestone as a row with Owner, Deadline and Status, filterable per owner and editable in place. An Analytics view is the third: total hours,@@ -219,7 +220,7 @@ rather than a category. - **Fit** button beside the cell-size picker: picks the finest grain whose whole span fits the pane, so the plan is fully visible with no horizontal scrolling. Chart-only - **Gantt / Table / Analytics toggle** pinned first in the header, left of every conditional control so its position never shifts. Three lenses over the same items: the chart answers "when", the table answers "whose, and by when", and analytics answers "how much, and is any of it in trouble". See The table view and The analytics view below - **Auto-schedule** button, with the plan-level controls next to Fit, in the chart and the table lenses. One press lays the WHOLE plan out under two rules at once: every task with dependencies starts BACK TO BACK with its latest predecessor, on that predecessor's end date, and no task starts or ends on a Saturday, a Sunday or a US federal holiday. Back to back is a statement about the CHART: a bar runs from its start to its end and the end is the boundary it stops at, so a successor starting on that date draws flush against it with no gap and no overlap. Tasks move EARLIER as well as later, so slack is pulled in and a conflict is pushed out. A task with no dependencies is an ANCHOR and keeps its start (shifted off a weekend or a holiday if it was on one), because that is the one thing in a plan this cannot derive. Duration is preserved in WORKING days, not calendar days, so a five-day task laid across a weekend, or across Labor Day, still takes five working days: a predecessor ending on the Friday hands off on the Friday and the weekend falls inside the successor's own span, never between the two. Milestones take the same rule with no duration: off a weekend or a holiday to the next working day, and on the day their last dependency ends. The graph crosses sections freely and so does the schedule; a section's displayed range just follows its rows. A dependency CYCLE is left exactly as it is, along with anything downstream of it, and the toast names the rows: there is no honest answer for a row that waits on itself. The whole pass is ONE undo entry and ONE changelog event (`action: "auto-schedule"`, with a count and the names of what moved), and a press that moves nothing writes nothing. Deterministic. Disabled, with a tooltip that says why, when the plan has no dependencies-- **Critical** button, next to it: a toggle that lights up the chain of work with no slack in it, the one that decides the finish date. Classic CPM forward and backward over BUSINESS days, on the dates the plan has right now, so it is independent of Auto-schedule and never moves anything. It uses the SAME finish-to-start lag Auto-schedule schedules with (a successor's earliest start is its predecessor's early finish, not the day after), so the two buttons can never disagree about the same plan: auto-schedule it and the chain that was just laid out tight reads as zero float. Total float of zero is critical; only rows in the dependency graph can be. The bars, the arrows along the path and any milestone on it glow white and breathe on a 2.6s cycle, in a saturated indigo in light mode and on paper, and the table lens marks the same rows on their left edge. Purely additive: selection outlines, dependency-mode amber and armed red all still read on a glowing row, and hit-testing is untouched. It recomputes live as dates and dependencies change. Disabled with the same reasoning when the plan has no dependencies+- **Critical** button, next to it: the LONGEST run of work in the plan. The dependency graph is a DAG, a task WEIGHS its duration in working days (weekends and observed US federal holidays out, the same arithmetic everything else in the app counts with) and a milestone weighs nothing, and the critical path is the maximum-weight path through it, found by the classic longest-path DP. It reads the dates the plan has right now, so it is independent of Auto-schedule and never moves anything. **This is not zero-float CPM** and it deliberately stopped being it in 0.49.0: CPM anchors its backward pass on the plan's latest finish, so it always answers "which chain ends last", and on a real plan that picks a short simple chain running into December over a ten-row interconnected build finishing in November. An isolated row is a path of one and can win if it is genuinely the longest thing there is. **One button, THREE states, cycled** the way the milestone style is, with the label and the tooltip naming the state you are in: off, then **Critical: plan** (one path, the longest in the whole plan), then **Critical: sections** (one longest path inside EVERY section at once, all lit together). Per section the lane is answered on its OWN subgraph: the nodes are that section's rows and the edges are the dependencies whose two ends are both in it, so a dependency that crosses sections is not an edge there and can never light up in that mode, and a section with no internal dependencies lights its longest single ROW, which is the same rule rather than an exception to it. Ties are broken deterministically, in this order: more working days, then a greater calendar span (first start to last end), then more rows, then whichever begins earlier in plan order, so exactly one path is ever lit per answer. The bars, the arrows along the path and any milestone on it glow white and breathe on a 2.6s cycle, in a saturated indigo in light mode and on paper, and the table lens marks the same rows on their left edge. Purely additive: selection outlines, dependency-mode amber and armed red all still read on a glowing row, and hit-testing is untouched. It recomputes live as dates and dependencies change, in both modes, and the mode is a saved preference beside the theme and the milestone style. A collapsed section hides its own glow with its rows and the rest of the plan keeps glowing. Disabled, with the same message it has always used, when the plan has no dependencies at all: a path is a run of work THROUGH dependencies, and a plan where nothing waits on anything has no runs to be longest - **Click legend** to filter phases, milestones, dependency arrows, or the today line - **Click a bar** to open the date editor with start/end pickers and dependency management. It is a compact 256px square (`--ed-sq`, one token for both dimensions): the row's name across the top, the two dates, the note, the dependency list and the buttons, at desktop density, so it covers as little of the chart as it can. The dependency list is the only part that flexes and the only part that scrolls; the buttons are pinned to the bottom edge. The milestone panel and the batch panel take the same compact metrics; the full **Edit item** panel, which is filled in rather than glanced at, keeps the full ones - **Drag bar edges** to resize (changes start or end date). Every timeline drag snaps to the grid the axis is currently drawing: day boundaries at the day cell size, week starts at weeks, month, quarter and year firsts at the coarser ones. Resizing the start leaves the end where it is and the mirror for the end; dragging the bar body snaps the START and preserves the DURATION, so the end lands at start plus the same number of days. The preview snaps live, line to line, and is exactly what gets committed. The shortest bar a resize can produce is the first grid line past the fixed edge, so it is never zero or negative. Milestones snap the same way. Dates set explicitly (table cells, the item modal, the batch editor, the project-start shift) are taken exactly as given and are never rounded@@ -352,7 +353,7 @@ read from a list, and the observance shift crosses the year boundary: New Year's is a Saturday, so it is observed on Friday 31 December 2027. They count as non-working everywhere: Auto-schedule steps over them, the critical path-counts in working days without them, and every hours figure excludes them. On the chart at+weighs its nodes in working days without them, and every hours figure excludes them. On the chart at day grain they get a weekend-style band in a different colour, with the holiday's name in a tooltip on the shaded date above the chart, and a `US holiday` key in the legend.
docs/BUILD-NOTES.md+219@@ -3,6 +3,225 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-20 (0.49.0, the critical path is the LONGEST path)++One change from Drew, client-side. `gantt.html` only. `server.js` did not move: nothing+about the sidecar, the changelog or the save protocol changed. **No live server was+restarted and no live plan was opened at any point.** Every suite runs on synthetic+fixtures on its own port.++Drew's report:++> how is critical path defined? Currently it seems to be the path w/ task ending the+> latest overall, but that is a relatively short/simple path compared to some of the+> longer/interconnected builds. I want Critical Path to be defined as the longest (start+> to end) path in the plan, and I want two modes: 1. Critical Path (global), 2. Critical+> Path (one for each phase). A single button should cycle through these, similar to how+> the milestone type cycles.++He read the old behaviour exactly right, and the reason is structural rather than a bug.++### Why the old definition always picked the chain that ends last++Through 0.48.0 this was textbook CPM: a forward pass for early start and early finish, a+backward pass for late start and late finish, and every row whose total float came out+zero lit up. The backward pass has to anchor somewhere, and what it anchors on is the+project's LATEST finish. So the only rows that can come out with zero float are the ones+on a chain that reaches that finish with nothing to spare. Everything else, however long,+however interconnected, has float against it by construction.++That is a correct answer to "what is driving the end date". It is not an answer to "what+is the longest piece of work in this plan", and on a real plan the two come apart badly:+a two-row administrative chain that happens to run into December beats a ten-row build+that finishes in November, every time.++Reproduced, on the 0.48.0 build, against the 0.49.0 fixture (`g49/oldnew.mjs`):++```+0.48.0 bars: p1, p2 15 working days, ends 20 November+0.49.0 bars: q1 q2 q3 q4 q5 q6 r1 r2 r3 85 working days, ends 16 October+```++### The new definition, exactly++The dependency graph is a DAG. Tasks and milestones are NODES; a `deps` entry between two+dated rows is an EDGE. It is the same `depGraph()` Auto-schedule reads, so the two+features still cannot disagree about what the plan's edges are.++* A node **weighs its own duration in WORKING days**: inclusive business days with+ weekends and observed US federal holidays taken out, the same `isOffDay` arithmetic+ 0.45.0 put behind every other count in the app. A milestone is an instant, so it+ weighs 0.+* A path's **length is the sum of the weights of the nodes along it**.+* The **critical path is the maximum-weight path in the DAG**, found by the classic+ longest-path dynamic program over a topological order.++Nothing is rescheduled and no date is read except the ones the plan has right now,+exactly as before. Rows on or downstream of a cycle are left out, because there is no+topological order through them, and they are reported in the status line the way they+always were.++Two breaks with the old code fall out of this and are deliberate:++**An isolated row is a path of one.** The old code only ever considered rows that were in+the dependency graph, on the reasoning that an isolated task is not a chain through+anything. Under a longest-path definition a one-node path is a real path, so it competes,+and it wins if it is genuinely the longest thing in the plan. On any plan with a chain+worth the name it does not, and it did not under CPM either.++**There is exactly ONE path, not a set of zero-float rows.** CPM lights everything with+zero float, which can be several chains at once. Drew asked for THE critical path, so the+answer is a single path and the tie-breaks below decide it.++### The tie-breaks++Applied in order, both to whole candidate paths and to the prefixes the DP carries, so+the answer is deterministic and reproducible rather than a function of the order rows+happen to sit in the file:++1. more working days (the definition itself)+2. greater CALENDAR span, first start to last end+3. more nodes+4. earliest first node in plan order++Rule 4 is total: two different paths cannot start at the same row and still be tied, so+one path is always picked. Two disjoint maxima therefore never both light up.++The running maximum is taken over EVERY node rather than only over sinks, and that is not+a shortcut. Node weights are never negative, and extending a path can only widen its+calendar span and add to its count, so a prefix can never beat its own extension under+the comparator above. Taking the maximum everywhere lands on a maximal path anyway, and+it is what carries a trailing zero-weight milestone onto the answer rather than stopping+at the task before it.++### Per section: the subgraph rule++The per-section mode computes each section's longest path **inside that section's own+subgraph**: the nodes are that section's rows, and the edges are the dependencies whose+two ends are BOTH in it. A dependency that crosses sections is not an edge there and can+never light up in that mode.++That is the point of the mode rather than a limitation of it. The question being asked is+"what is the longest run of work inside this section", and a chain that leaves the section+is not an answer to it. The global mode is where crossing dependencies belong, and it+lights them: on the fixture, `q6 -> r1` crosses from The long build into Integration and+is one of the eight arrows the global answer draws.++A section whose rows depend on nothing inside it therefore has no edges at all, and its+longest path is its single longest ROW. That is the same rule, not an exception to it, and+it is what gets lit. The fixture's Loose ends lane is three unlinked tasks of 3, 8 and 5+working days, and the 8 is what glows.++All the sections glow **at once**, and all of them in the same white (indigo on paper).+They are not colour-coded per section: the lanes already say which section a row is in,+and a second colour system over the top would be saying it twice.++### One button, three states++Off, then the whole plan, then one per section, then off. It is deliberately the SAME+control the milestone style is, because Drew asked for it in those terms and because the+app already had one cycling button and should not grow a second kind:++| | milestone style | critical path |+|---|---|---|+| element | `.act-btn` in the header | `.act-btn` in the header |+| states | `MS_STYLES`, cycled by index | `CRIT_MODES`, cycled by index |+| state channel | `data-ms` on the root | `data-crit` on the root |+| label | `#msStyleLabel`, rewritten per state | `#critLabel`, rewritten per state |+| tooltip | says what the button does | says what state you are in and what the next press does |+| width | pinned, so pressing it moves nothing | pinned, so pressing it moves nothing |+| persistence | `prefs.msStyle`, one localStorage key | `prefs.critMode`, the same key |++The labels are `Critical`, `Critical: plan` and `Critical: sections`. The one thing the+critical button adds is `.on`, which the milestone button has no use for: two of these+three states lay a treatment over the plan, and the reader has to be able to tell from+the button alone that something on the chart is lit. `aria-pressed` follows it.++`data-crit` used to be `on` / `off`; it is now `off` / `global` / `phase`. No CSS has ever+read it and no suite asserted it, so widening it costs nothing and it is now the whole+state rather than half of it.++### Persistence, and the one case where it is dropped++`prefs.critMode` sits in the same localStorage key as the theme, the grain and the+milestone style, and it is clamped on the way in the same way, so a hand-edited or stale+value lands on `off` rather than on a mode with no code behind it. A reader who left the+longest path lit finds it lit when they come back.++It is **painted rather than re-entered** on load: `setCritical` flashes the status line,+and a message about a chart the reader has only just opened is noise.++One exception. That preference key is shared by every plan this browser opens, and the+button is disabled on a plan with no dependencies at all. So on load a saved mode is+dropped when `planHasDeps()` is false, and dropped **in memory only**: opening a+dependency-free chart must not silently un-choose the mode for the plan the reader was+actually working on.++### What did NOT change++* **Auto-schedule.** The 0.43.0 zero-lag scheduling is untouched. Only the highlight's+ definition moved; the two buttons are still independent and neither reads the other.+* **The glow.** Every rule under `== The critical path ==` in the stylesheet is+ byte-identical to 0.42.0: `critBar`, `critStroke`, `critDiamond`, `critRule`, `critCap`+ and `critRow`, all on the same 2.6s breath, white on screen and `#4c1d95` on paper, and+ the reduced-motion fallback with them. A/B'd as computed style in both themes and in+ both modes.+* **The disabled gate.** Still `planHasDeps()`, still the same message. The definition+ changed but the question did not: a path is a run of work THROUGH dependencies, and a+ plan where nothing waits on anything has no runs to be longest. A plan like that does+ have a longest task, but calling that "the critical path" would be a worse answer than+ saying there isn't one.+* **Everything additive.** Selection outlines, dependency-mode amber, armed red and+ hit-testing are all exactly what they were on a glowing row.+* **Collapse.** The glow is a class on elements inside rows, so a collapsed lane takes its+ own glow off screen with its rows under the 0.34.0 rules and the class stays on for when+ the lane opens again. In per-section mode the other lanes keep glowing. In global mode+ the answer itself does not change; the arrows follow `drawDeps`, which draws only the+ links between two visible rows, exactly as it always has.++### The suites, and every assertion that was rewritten++New: `g49/v49.mjs`, 114 assertions on a new bench (port 9011, with a dependency-free twin+on 9015), plus `g49/oldnew.mjs`, which is the old-versus-new recording quoted above.++The fixture is built to make the two definitions disagree rather than accidentally agree:+a two-row 15-day chain that ends last, a six-row 66-day build that finishes two months+earlier and runs on through a third lane to 85 days, a branch that must never light, a+lane with no dependencies at all, a dead tie broken by plan order and a tie broken by+calendar span. `q5` spans 31 August to 11 September: twelve calendar days, ten weekdays+and **nine working days**, because Labor Day is one of them, so the reported total of 85+is only correct if the weekends and the holiday were both taken out.++Every expected number is hand-computed and then re-derived by `g49/lib49.mjs`, which is a+second implementation of the business-day and federal-holiday arithmetic that walks the+calendar rather than closing the form. The app and the suite can only agree by both being+right.++**The v42 and v43 assertions were re-examined line by line rather than assumed.** They+assert the old zero-float CPM, so each one had to be re-derived under the new definition:++| assertion | old reason | new reason | outcome |+|---|---|---|---|+| `v42` B1 bars `c1, c2, c4` | zero float against a finish of 17 | longest path is `c1+c2+c4 = 20` working days, against 17 for the a-chain and 5 for the longest isolated row | **same set**, comment rewritten |+| `v42` B1 arrows `c1>c2, c2>c4` | both ends critical and flush | both links are on the maximum-weight path | **same set**, comment rewritten |+| `v42` B1 milestone `C5 ship` | zero float, zero duration | weighs 0 and extends the winning path, so the running maximum carries it | **same**, comment rewritten |+| `v42` B6 bars `c1, c3, c4` after c3 is given 15 days | float moved to c2 | `c1+c3+c4 = 25` beats `c1+c2+c4 = 20` | **same set**, comment rewritten |+| `v42` B7 table rows | the same four rows | the same four rows | **same** |+| `v42big` disabled title | no dependencies | no dependencies | **unchanged on purpose**, see the disabled gate above |+| `v43` G6 bars, arrows, milestone | zero float on the plan Auto-schedule just laid out | longest path on that plan: `5+10+5 = 20` beats the a-chain's 17 | **same set**, comment rewritten |+| `v43` G6 "c3 has real float and is dark" | five days of float | not on the maximum-weight path: the way round through c2 is worth five more working days | **same outcome**, reason rewritten |+| `v45` D4 bars `s1, s2, s3` | the only chain, laid out tight | `5+5+2 = 12` beats the longest isolated row in the plan, which is `c6` at 8 | **same set**, comment rewritten |++So no expected SET moved. That is worth saying plainly rather than quietly: those three+fixtures each have one chain that is both the longest and the last-ending, which is+exactly the case where the two definitions coincide, and it is why the round needed a new+fixture built to separate them. Every one of those comments now derives its expectation+from working-day weights instead of from float, so a future reader is not left with a+suite that explains itself in terms of a rule the app no longer follows.++`mssel.js` and `msphase.js` are fixture-coupled and still report NEEDS REVIEW with an+empty `jsErrors`, byte-identical to 0.48.0.+ ## Build notes, 2026-08-20 (0.48.0, Auto-schedule says it is running) One change from Drew, client-side. `gantt.html` only. `server.js` did not move: nothing
gantt.html+248−76@@ -330,6 +330,11 @@ header .new-btn { display: flex; align-items: center; gap: 6px; background: var( header .new-btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); } header .new-btn svg { width: 13px; height: 13px; } header #btnMsStyle { min-width: 96px; justify-content: center; }+/* Pinned for the same reason the milestone button is: the label names the state, the+ three names are not the same width, and .actions to the right of it is right-aligned,+ so an unpinned button would drag its neighbours sideways every time it was pressed.+ Sized to the longest of the three. */+header #btnCrit { min-width: 158px; justify-content: center; } header #btnTheme { min-width: 56px; justify-content: center; } /* Below this the header is out of room and the project's own name starts to truncate, which is the one thing in here that must not. Shed chrome instead:@@ -348,6 +353,9 @@ header #btnTheme { min-width: 56px; justify-content: center; } alone dragged the Fit button sideways. Theme keeps its word rather than becoming a blank button. */ header #btnMsStyle { min-width: 36px; }+ /* Icon-only too, and pinned at the same width, so the plan group stops moving under+ the reader once the labels are gone. */+ header #btnCrit { min-width: 36px; } } /* Narrower still: an abbreviated subtitle is worse than no subtitle. Drop it and keep the title whole. The full text stays on the header's tooltip. */@@ -520,7 +528,7 @@ body.col-resizing { cursor: col-resize; user-select: none; } .dep-svg polygon { fill: var(--accent); opacity: 0.5; transition: opacity 0.15s; } .dep-svg.hidden path, .dep-svg.hidden polygon { opacity: 0; } /* == The critical path ==- The chain with no slack in it, breathing. Everything here is ADDITIVE: the glow is+ The longest run of work in the plan, breathing. Everything here is ADDITIVE: the glow is carried on box-shadow and filter, which no other state in this app uses on these elements, so a bar can be critical AND selected AND armed at once and each of the three still says what it says. The two dependency-mode treatments below deliberately@@ -1442,7 +1450,8 @@ function savedPrefs(){ // step buttons have somewhere definite to land. const FS_STEPS = [0.85, 0.925, 1, 1.1, 1.2, 1.35, 1.5]; const prefs = Object.assign(- { theme: 'dark', msStyle: 'line', granularity: 'month', showToday: true, view: 'gantt', fs: 1, collapsed: [] },+ { theme: 'dark', msStyle: 'line', granularity: 'month', showToday: true, view: 'gantt', fs: 1, collapsed: [],+ critMode: 'off' }, viewDefaults(), savedPrefs() );@@ -1456,6 +1465,19 @@ const MS_STYLES = ['line', 'diamond', 'both']; function clampMsStylePref(){ if (MS_STYLES.indexOf(prefs.msStyle) < 0) prefs.msStyle = 'line'; }+// The three critical-path states, in the order the button cycles them, and they are a+// PREFERENCE for exactly the reason the milestone style is one: it is a way of looking+// at the plan rather than a change to it, so a reader who left the longest path lit+// should find it lit when they come back. Same storage, same clamp, same shape: a+// hand-edited or stale localStorage value lands on the first one, which is off.+const CRIT_MODES = ['off', 'global', 'phase'];+function clampCritModePref(){+if (CRIT_MODES.indexOf(prefs.critMode) < 0) prefs.critMode = 'off';+}+// Is anything lit at all. Everything that used to test the old `critOn` flag asks THIS,+// so there is still exactly one answer to "is the glow up" and the mode itself is the+// only state there is.+function critOn(){ return prefs.critMode !== 'off'; } // Which halves the current style is showing. Everything that measures or lays out the // dashed rule and its cap asks this rather than testing for 'line', because 'both' // shows them too and a test for one name would have left the caps unplaced there.@@ -1464,6 +1486,9 @@ function savePrefs(){ try { localStorage.setItem(PREF_KEY, JSON.stringify(prefs) function applyTheme(){ document.documentElement.setAttribute('data-theme', prefs.theme); } function applyStartupPrefs(){ applyTheme();+// The critical-path mode is on the root the same way the theme and the milestone style+// are, so the state is readable from the document rather than only from a closure.+document.documentElement.setAttribute('data-crit', prefs.critMode); // Both set before the chart is built: the view so a plan that opens as a table // never measures a hidden timeline, the scale so the first paint is already at // the size the user chose and nothing visibly resettles. The header buttons do@@ -1831,8 +1856,8 @@ $('hdr').innerHTML = ` <div class="plan-ctl"> <button class="act-btn" id="btnAuto" title="Align all task dates so dependencies line up finish-to-start, skipping weekends and US federal holidays"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h7M4 12h11M4 17h6"/><path d="M17 14l3 3-3 3"/><path d="M20 17h-6"/></svg><span class="btn-label">Auto-schedule</span></button>- <button class="act-btn" id="btnCrit" title="Critical path: light up the chain of work with no slack in it, the one that decides the finish date" aria-pressed="false">- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18l5-5 4 3 7-8"/><circle cx="4" cy="18" r="1.6"/><circle cx="9" cy="13" r="1.6"/><circle cx="13" cy="16" r="1.6"/><circle cx="20" cy="8" r="1.6"/></svg><span class="btn-label">Critical</span></button>+ <button class="act-btn" id="btnCrit" title="Critical path: light up the longest run of work in the plan, start to end. Press again for one path per section." aria-pressed="false">+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18l5-5 4 3 7-8"/><circle cx="4" cy="18" r="1.6"/><circle cx="9" cy="13" r="1.6"/><circle cx="13" cy="16" r="1.6"/><circle cx="20" cy="8" r="1.6"/></svg><span class="btn-label" id="critLabel">Critical</span></button> </div> <div class="actions"> <button class="act-btn" id="btnMsStyle" title="Milestone style, cycled: dashed lines with tags, diamonds on their own row, or both at once">@@ -3100,7 +3125,7 @@ function diffAndLog(via){ body:JSON.stringify({actor:'user',events:evs.map(stamp)})}) .catch(()=>{}); }-function scheduleSave(via){diffAndLog(via||'edit');if(critOn)refreshCritical();syncPlanCtl();if(prefs.view==='analytics')renderAnalytics();clearTimeout(saveTimer);saveTimer=setTimeout(()=>{saveTimer=null;saveToFile();},800);}+function scheduleSave(via){diffAndLog(via||'edit');if(critOn())refreshCritical();syncPlanCtl();if(prefs.view==='analytics')renderAnalytics();clearTimeout(saveTimer);saveTimer=setTimeout(()=>{saveTimer=null;saveToFile();},800);} // The changelog entry goes out immediately but the sidecar write waits 800ms, so // a tab closed inside that window left the log claiming an edit the sidecar never // received. Anything still pending goes out synchronously when the page is being@@ -6197,67 +6222,150 @@ function cycleNames(stuck){ return nm.slice(0,4).join(', ')+(nm.length>4?', and '+(nm.length-4)+' more':'')+' (dependency cycle)'; } -// ── Critical path ────────────────────────────────────────────────────────────+// ── Critical path: the LONGEST path in the plan ────────────────────── //-// Classic CPM, forward and backward, in BUSINESS days, over the dates the plan has-// right now. It never reschedules anything: the two buttons are independent, and a-// reader who wants to know which chain is driving the end date should be able to-// ask that of the plan as it stands and not of a plan this app rewrote first.+// 0.49.0 REDEFINES this. Through 0.48.0 it was textbook CPM: a forward and a backward+// pass, and every row with zero float lit up. The backward pass anchors on the plan's+// LATEST finish, so what that answered was always "which chain ends last". Drew's+// report is that on a real plan this picks a short, simple two-row chain that happens+// to run into December over a ten-row interconnected build that finishes in November,+// and the ten-row build is plainly the longer piece of work. //-// The forward pass takes each node's own start as a floor, so a row the author put-// later than its dependencies allow is respected rather than compressed. A row with-// no predecessors therefore simply keeps its own early start, which is what makes-// the numbers below match what is drawn.+// So the question the button answers now is the one he asked for: what is the LONGEST+// run of work in this plan. Precisely: //-// Only rows that are IN the dependency graph can be critical: an isolated task that-// happens to run past everything else is not a chain through anything. Durations-// are inclusive business days, so a chain's length is the sum of its rows' working-// days and a milestone contributes nothing.+// * The dependency graph is a DAG. Tasks and milestones are NODES; a `deps` entry+// between two dated rows is an EDGE. (Same depGraph() Auto-schedule reads, so the+// two features can still never disagree about what the plan's edges are.)+// * A node WEIGHS its own duration in WORKING days: inclusive business days with+// weekends and observed federal holidays taken out, the same isOffDay arithmetic+// 0.45.0 put behind every other count in this app. A milestone is an instant, so+// it weighs 0.+// * A path's LENGTH is the sum of the weights of the nodes along it. The critical+// path is the maximum-weight path in the DAG, found by the classic longest-path+// DP over a topological order. //-// The finish-to-start LAG here is the one Auto-schedule uses, and it has to be, or-// the two buttons would answer different questions about the same plan: press one-// and the other would report float on the chain it had just laid out tight. A-// successor starts ON its predecessor's end, so the earliest a successor can start-// is EF(pred) and not EF(pred)+1, and the mirror holds on the backward pass.-function criticalSet(){+// Nothing is rescheduled and no date is read except the ones the plan has right now,+// exactly as before. Rows on or downstream of a cycle are left out, because there is+// no topological order through them; they are reported the way they always were.+//+// TWO MODES, because a plan is read at two altitudes:+//+// 'global' one path, the longest in the whole plan.+// 'phase' one path per SECTION, each computed inside that section's own subgraph:+// the nodes are that section's rows and the edges are the `deps` whose two+// ends are BOTH in it. A dependency that crosses sections is deliberately+// not an edge here and can never light up in this mode, because the+// question being asked is "what is the longest run of work inside this+// section" and a chain that leaves the section is not an answer to it.+// A section whose rows depend on nothing inside it therefore has no edges+// at all, and its longest path is its single longest ROW. That is the same+// rule, not an exception to it, and it is what gets lit.+//+// AN ISOLATED ROW IS A PATH OF ONE. This is the second break with the old code, which+// only ever considered rows that were in the graph. A one-node path is a real path and+// it wins if it is genuinely the longest thing in the plan; on any plan with a chain+// worth the name it is not, and it never was under CPM either.+//+// TIE-BREAKS, in order, applied to whole candidate paths and to the prefixes the DP+// carries, so the answer is deterministic and reproducible rather than a function of+// the order rows happen to sit in the file:+//+// 1. more working days (the definition itself)+// 2. greater CALENDAR span, first start to last end+// 3. more nodes+// 4. earliest first node in plan order (stable, and total: two paths cannot start+// at the same row and still be tied here)+//+// When two disjoint paths are still tied after 1-3, rule 4 picks exactly one, because+// Drew asked for THE critical path and two of them is not an answer.+function nodeDays(n){ return n.ms?0:Math.max(1,bizBetween(n.start,n.end)); }+// True when path a beats path b. Both are DP records; `b` is null on the first+// candidate, which a always beats.+function pathBeats(a,b){+ if(!b)return true;+ if(a.days!==b.days)return a.days>b.days;+ if(a.span!==b.span)return a.span>b.span;+ if(a.count!==b.count)return a.count>b.count;+ return a.first<b.first;+}+// The longest path through `order` (a topological order), restricted to `members` when+// one is given. One pass: every node's best incoming prefix is chosen from its+// predecessors' already-decided ones, which is sound because a topological order puts+// every predecessor first.+//+// The running maximum is taken over EVERY node rather than only over sinks, and that is+// not a shortcut: node weights are never negative and extending a path can only widen+// its calendar span and add to its count, so a prefix can never beat its own extension+// under the comparator above. Taking the maximum everywhere therefore lands on a+// maximal path anyway, and it is what carries a trailing zero-weight milestone onto the+// answer rather than stopping at the task before it.+function longestPath(order,members,idx){+ const best=new Map();+ let win=null;+ order.forEach(n=>{+ if(members&&!members.has(n))return;+ const d=nodeDays(n),s=dayNum(n.start),e=dayNum(n.end);+ let rec=null;+ n.preds.forEach(p=>{+ if(members&&!members.has(p))return;+ const b=best.get(p);+ if(!b)return; // on or downstream of a cycle: no order, no path+ const c={days:b.days+d,minS:Math.min(b.minS,s),maxE:Math.max(b.maxE,e),+ count:b.count+1,first:b.first,node:n,prev:b};+ c.span=c.maxE-c.minS;+ if(pathBeats(c,rec))rec=c;+ });+ if(!rec)rec={days:d,minS:s,maxE:e,span:e-s,count:1,first:idx.get(n),node:n,prev:null};+ best.set(n,rec);+ if(pathBeats(rec,win))win=rec;+ });+ if(!win)return null;+ // Walk the chosen record back to its start. The EDGE ids are collected on the way, so+ // only the links actually ON the path are ever lit: two chains that touch cannot make+ // the link between them glow, which is the same guarantee the old edge test gave.+ const crit=new Set(),edges=new Set();+ for(let r=win;r;r=r.prev){+ crit.add(r.node);+ if(r.prev&&r.prev.node.id&&r.node.id)edges.add(r.prev.node.id+'>'+r.node.id);+ }+ return {crit,edges,days:win.days,count:win.count};+}+// One or many paths, depending on the mode, unioned into the one set the painters read.+// The per-path detail is kept alongside it for the message the button flashes.+function criticalSet(mode){ const g=depGraph();- const inGraph=g.nodes.filter(n=>n.preds.length||n.succs.length);- if(!inGraph.length)return {crit:new Set(),edges:new Set(),cycle:[],float:new Map()}; const {order,stuck}=topoOrder(g);- const live=order.filter(n=>n.preds.length||n.succs.length);- const dur=new Map(),ES=new Map(),EF=new Map(),LS=new Map(),LF=new Map();- live.forEach(n=>dur.set(n,n.ms?0:Math.max(1,bizBetween(n.start,n.end))));- // Business-day INDEX, so every comparison below is an integer count of working- // days and the weekend never enters the arithmetic. Anchored at the earliest date- // in the plan; only differences are ever used, so the anchor itself is arbitrary.- const base=live.reduce((m,n)=>(m===null||dayNum(n.start)<dayNum(m))?n.start:m,null);- const idx=ds=>bizBetween(base,nextBiz(ds))-1;- live.forEach(n=>{- let es=idx(n.start);- n.preds.forEach(p=>{ if(EF.has(p))es=Math.max(es,EF.get(p)); });- ES.set(n,es);- EF.set(n,es+Math.max(dur.get(n),1)-1);- });- const finish=live.reduce((m,n)=>Math.max(m,EF.get(n)),-Infinity);- for(let i=live.length-1;i>=0;i--){- const n=live[i];- let lf=finish;- n.succs.forEach(s=>{ if(LS.has(s))lf=Math.min(lf,LS.get(s)); });- LF.set(n,lf);- LS.set(n,lf-Math.max(dur.get(n),1)+1);+ const idx=new Map();+ g.nodes.forEach((n,i)=>idx.set(n,i));+ const paths=[];+ if(order.length){+ if(mode==='phase'){+ // Bucketed in ONE pass, and each bucket keeps the topological order it was cut+ // from, so a section's pass walks only that section's rows and still sees every+ // predecessor before the row that waits on it. Visited top to bottom, and a+ // section with no dated rows simply never appears.+ const byPhase=new Map();+ order.forEach(n=>{+ const ph=typeof n.r.phase==='number'?n.r.phase:-1;+ if(!byPhase.has(ph))byPhase.set(ph,{seq:[],set:new Set()});+ const bag=byPhase.get(ph);+ bag.seq.push(n);bag.set.add(n);+ });+ [...byPhase.keys()].sort((a,b)=>a-b).forEach(ph=>{+ const bag=byPhase.get(ph);+ const w=longestPath(bag.seq,bag.set,idx);+ if(w){w.phase=ph;paths.push(w);}+ });+ }else{+ const w=longestPath(order,null,idx);+ if(w){w.phase=null;paths.push(w);}+ } }- const flt=new Map(),crit=new Set();- live.forEach(n=>{ const f=LS.get(n)-ES.get(n); flt.set(n,f); if(f===0)crit.add(n); });- // An EDGE is critical when both its ends are and the successor really does follow- // the predecessor with no slack between them. Without that last test two separate- // zero-float chains that happen to touch would light up the link between them.- const edges=new Set();- crit.forEach(n=>n.preds.forEach(p=>{- if(crit.has(p)&&EF.get(p)===ES.get(n)&&p.id&&n.id)edges.add(p.id+'>'+n.id);- }));- return {crit,edges,cycle:stuck,float:flt,finish,graph:g};+ const crit=new Set(),edges=new Set();+ paths.forEach(w=>{w.crit.forEach(n=>crit.add(n));w.edges.forEach(e=>edges.add(e));});+ return {crit,edges,cycle:stuck,paths,graph:g}; }-let critOn=false; // The glow is a CLASS on the elements that are already there, added and removed by // this one function, so there is exactly one place that decides what is lit. It is // idempotent: everything is cleared first, which is what makes "recompute" and@@ -6267,8 +6375,8 @@ function refreshCritical(){ document.querySelectorAll('.crit-ms').forEach(el=>el.classList.remove('crit-ms')); document.querySelectorAll('.data-table tr.crit-row').forEach(el=>el.classList.remove('crit-row')); if(depSvg)depSvg.querySelectorAll('.crit').forEach(el=>el.classList.remove('crit'));- if(!critOn){critInfo=null;return null;}- const info=criticalSet();+ if(!critOn()){critInfo=null;return null;}+ const info=criticalSet(prefs.critMode); critInfo=info; info.crit.forEach(n=>{ if(n.id&&barEls[n.id])barEls[n.id].bar.classList.add('crit');@@ -6287,7 +6395,7 @@ function refreshCritical(){ function paintCritDeps(){ if(!depSvg)return; depSvg.querySelectorAll('.crit').forEach(el=>el.classList.remove('crit'));- if(!critOn||!critInfo)return;+ if(!critOn()||!critInfo)return; depSvg.querySelectorAll('[data-from]').forEach(el=>{ if(critInfo.edges.has(el.getAttribute('data-from')+'>'+el.getAttribute('data-to')))el.classList.add('crit'); });@@ -6302,26 +6410,69 @@ let critInfo=null; function paintCritTable(){ if(!tableBody)return; tableBody.querySelectorAll('tr.crit-row').forEach(el=>el.classList.remove('crit-row'));- if(!critOn||!critInfo)return;+ if(!critOn()||!critInfo)return; const keys=new Set(); critInfo.crit.forEach(n=>keys.add(rowKey(n.r))); tableBody.querySelectorAll('tr[data-key]').forEach(tr=>{ if(keys.has(tr.dataset.key))tr.classList.add('crit-row'); }); }-function setCritical(on){- critOn=!!on;- document.documentElement.setAttribute('data-crit',critOn?'on':'off');+// One button, three states, cycled in CRIT_MODES order, and it is deliberately the+// SAME control the milestone style is: the label names the state you are in, the+// tooltip says what the next press will do, and the choice is a saved preference. The+// only thing this one adds is `.on`, because two of its three states are a treatment+// laid over the plan and the reader has to be able to see, from the button alone, that+// something on the chart is lit.+const CRIT_BTN = {+ off: {label:'Critical',+ title:'Critical path: light up the longest run of work in the plan, start to end. Press again for one path per section.'},+ global:{label:'Critical: plan',+ title:'Critical path: the longest run of work in the whole plan is lit. Press again for one path per section.'},+ phase: {label:'Critical: sections',+ title:'Critical path: the longest run of work INSIDE each section is lit, and a dependency that crosses sections is left out. Press again to turn it off.'}+};+function syncCritBtn(){+ const m=CRIT_BTN[prefs.critMode]||CRIT_BTN.off;+ const l=$('critLabel');+ if(l)l.textContent=m.label;+}+// Enter a state. Idempotent, and the one place the mode is written: the attribute, the+// button, the glow and the saved preference all follow from it.+function setCritical(mode){+ prefs.critMode=CRIT_MODES.indexOf(mode)<0?'off':mode;+ document.documentElement.setAttribute('data-crit',prefs.critMode);+ savePrefs();+ syncCritBtn(); const b=$('btnCrit');- if(b)b.classList.toggle('on',critOn);+ if(b){b.classList.toggle('on',critOn());b.setAttribute('aria-pressed',critOn()?'true':'false');}+ syncPlanCtl(); const info=refreshCritical();- if(critOn&&info){- const n=info.crit.size;- let msg=n?'Critical path: '+n+' item'+(n===1?'':'s')+', no slack':'No critical path: nothing in this plan depends on anything';- if(info.cycle&&info.cycle.length)msg+='. '+info.cycle.length+' left out: '+cycleNames(info.cycle);- flash(saveInd,msg,info.cycle&&info.cycle.length?6000:3200);+ if(critOn()&&info){+ flash(saveInd,critMessage(info),info.cycle&&info.cycle.length?6000:3200); } }+function cycleCritical(){+ setCritical(CRIT_MODES[(CRIT_MODES.indexOf(prefs.critMode)+1)%CRIT_MODES.length]);+}+// What the status line says on the way in. It reports the LENGTH, in working days,+// because that is now the whole definition and a reader who cannot see the number has+// no way to check the app against their own arithmetic.+function critMessage(info){+ let msg;+ if(!info.paths.length){+ msg='No critical path: nothing in this plan is dated';+ }else if(prefs.critMode==='phase'){+ const items=info.crit.size;+ msg='Longest path in each section: '+info.paths.length+' section'+(info.paths.length===1?'':'s')+', '+ +items+' item'+(items===1?'':'s')+', longest is '+ +Math.max(...info.paths.map(w=>w.days))+' working days';+ }else{+ const w=info.paths[0];+ msg='Longest path: '+w.count+' item'+(w.count===1?'':'s')+', '+w.days+' working day'+(w.days===1?'':'s');+ }+ if(info.cycle&&info.cycle.length)msg+='. '+info.cycle.length+' left out: '+cycleNames(info.cycle);+ return msg;+} function syncThemeBtn(){ $('themeLabel').textContent = prefs.theme==='dark' ? 'Light' : 'Dark'; } function wireThemeButton(){@@ -8465,7 +8616,7 @@ function setView(v){ // The bars and the arrows were rebuilt while the glow was off screen, so it goes // back on with them. Deferred behind the relayout, since the arrows do not exist // until it has run.- if(critOn)setTimeout(()=>refreshCritical(),0);+ if(critOn())setTimeout(()=>refreshCritical(),0); } // ── The two plan-level buttons ── //@@ -8485,17 +8636,22 @@ function syncPlanCtl(){ :'Auto-schedule needs dependencies: nothing in this plan waits on anything yet'; } if(c){+ // Still gated on the plan having edges at all. The definition changed, but the+ // question did not: a PATH is a run of work through dependencies, and a plan where+ // nothing waits on anything has no runs to be longest. The message is the one it+ // has always been, for the same reason. c.disabled=!has;- c.title=has?'Critical path: light up the chain of work with no slack in it, the one that decides the finish date'+ c.title=has?(CRIT_BTN[prefs.critMode]||CRIT_BTN.off).title :'The critical path needs dependencies: nothing in this plan waits on anything yet';- c.setAttribute('aria-pressed',critOn?'true':'false');- c.classList.toggle('on',critOn);+ c.setAttribute('aria-pressed',critOn()?'true':'false');+ c.classList.toggle('on',critOn()); } } function wirePlanCtl(){ const a=$('btnAuto'), c=$('btnCrit'); if(a)a.addEventListener('click',()=>{ if(!a.disabled)runAutoScheduleBusy(); });- if(c)c.addEventListener('click',()=>{ if(!c.disabled)setCritical(!critOn); });+ if(c)c.addEventListener('click',()=>{ if(!c.disabled)cycleCritical(); });+ syncCritBtn(); syncPlanCtl(); } async function startApp(){@@ -8528,6 +8684,21 @@ renderTable(); // The sidecar can add or drop a dependency, so what the two plan buttons are allowed // to do is only settled once it is in. syncPlanCtl();+// The saved critical-path mode comes back with the plan, the way the saved milestone+// style does. It is PAINTED rather than re-entered: setCritical would flash the status+// line, and a message about a chart the reader has only just opened is noise.+//+// A plan with no dependencies at all cannot carry a path, so a mode saved on some other+// chart is dropped for this one. Dropped in memory only, deliberately: the preference is+// one key for every plan this browser opens, and opening a dependency-free chart must+// not silently un-choose the mode for the plan the reader was actually working on.+if(critOn()&&!planHasDeps()){+ prefs.critMode='off';+ document.documentElement.setAttribute('data-crit','off');+ syncPlanCtl();+}+syncCritBtn();+if(critOn())refreshCritical(); if(prefs.view==='analytics')renderAnalytics(); // Say it after the first paint, so the message sits over a chart that clearly // still works rather than over a blank page.@@ -8569,6 +8740,7 @@ async function init(){ dropUndatedRows(); clampFontScalePref(); clampMsStylePref();+ clampCritModePref(); applyStartupPrefs(); wireLaneToggle(); initTimeRange();
package.json+2−2@@ -2,8 +2,8 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.48.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 working days, skipping weekends and US federal holidays, 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 total, remaining and average hours, one row per person carrying both their hours and their status counts, 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.",+ "version": "0.49.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 working days, skipping weekends and US federal holidays, and one more lights up the critical path, meaning the longest run of work in the plan, either once across the whole plan or once inside every section. 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 total, remaining and average hours, one row per person carrying both their hours and their status counts, 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": { "adom-gantt": "server.js"
page.json+3−3@@ -2,8 +2,8 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.48.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 working days, skipping weekends and US federal holidays, 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 total, remaining and average hours, one row per person carrying both their hours and their status counts, 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.",+ "version": "0.49.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 working days, skipping weekends and US federal holidays, and one more lights up the critical path, meaning the longest run of work in the plan, either once across the whole plan or once inside every section. 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 total, remaining and average hours, one row per person carrying both their hours and their status counts, 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": { "adom-gantt": "server.js"@@ -85,4 +85,4 @@ "name": "Drew Owens", "email": "[email protected]" }-}+}