← Commit history

Publish 0.70.0

Drew Owens ·5269c77444 · ·parent 9719024
7 files changed +100−9
README.md+2
@@ -64,6 +64,8 @@ A plain on/off toggle. Each lane is answered on its OWN subgraph, so a dependenc  **Dependency arrows.** Teal routes with rounded elbows and arrowheads show task relationships: out of the source bar's end sideways, down, and into the next bar's start sideways again, steering round any bar in the way rather than through it. Click any bar to see incoming ("needs") and outgoing ("blocks") dependencies. Add or remove dependencies inline. +**Current and Overdue.** Two chips in the legend, for reviewing a plan with the team. **Current** (0.70.0) shows only what has started (start date today or earlier) and is not complete or cancelled: the work under way, plus anything that should have started and has not. Future and finished rows are hidden, in the chart and the table. **Overdue** shows only what is past its deadline and not done. Turn both on for just the overdue rows. Each counts tasks and milestones separately, and neither changes the plan.+ **Legend filtering.** Click any phase, "Milestone", "Dependencies" or "Today" in the legend bar to toggle visibility. Dimmed items fade to 15% opacity; hiding "Today" removes the marker line. The legend wraps onto as many lines as the plan needs, so a plan with twenty sections shows all twenty chips. When the sections carry tags (0.59.0), the legend leads with a chip per tag and the twenty section chips fold behind one "20 sections" chip; pick a tag and only the sections carrying it are shown, in the chart and the table, with their own chips back in the legend. Pick several to see any of them; Clear tags shows everything. It is your view only, and it survives a refresh. Since 0.64.0 the legend also leads with an **Owner** pick (every owner with their row count, plus **Unassigned**, shared with the table's owner filter) and an **Overdue** chip: rows whose end date or milestone date has passed without being marked complete or cancelled get a red edge and ring, and the chip shows only them. Overdue is worked out from the dates every time the page draws and at midnight; it is never written into the plan.  **How long, and how long left.** The header says **Project Duration** and **Time Remaining**, worked out from the plan's own first and last dates and today, never from the cell size, leaving cancelled work out, and updated on every edit. They stay whole at every window width: the header sheds button words, then wraps, before it lets them be cut. A printed chart names the plan in a title line of its own. Hover either for the exact dates and the calendar and working-day counts. The plan itself is named once, in the brand block at the far left; its title and description are on that block's tooltip.
SKILL.md+1
@@ -240,6 +240,7 @@ rather than a category. - **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, an on/off TOGGLE**, with one constant label and the state carried by its pressed treatment: off, or one longest path inside EVERY section at once, all lit together. 0.49.0 offered a plan-wide reading as a third state of the same button and 0.50.0 dropped it, because on a real plan it lights one section's chain and leaves every other section dark, which is a narrower reading than the per-section answer that contains it. With no label to swap, the button sizes off its own content like every other header action rather than being pinned to the widest of three words. Each 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. **CANCELLED WORK IS NOT A NODE, since 0.53.0:** it can never glow, it carries no weight, and a chain that ran through it is BROKEN at that row, since the node is absent and its predecessors and successors are no longer joined by way of it. A section with a second route round the cancelled row is REROUTED onto it; a section whose only route ran through it falls back to the longest single row left, which may be neither of the two stumps' first halves. The longest-single-row fallback ignores cancelled rows for the same reason: they are not there to be considered. Nothing is deleted from the data, so un-cancelling puts the node, its links and every path through it straight back. 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. A GAP adds no weight, because a path's length is the sum of the NODES along it and not the calendar it is spread over, so the gaps 0.53.0's push-only Auto-schedule now preserves cannot promote or demote a chain; the calendar span is reachable only by the second tie-break, where it separates two chains already tied on working days and says the one occupying more of the calendar is the more critical, which is the reading it has always had. 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, and the state is a saved preference beside the theme and the milestone style; a browser that stored the old plan-wide mode comes back lit, since that reader had the glow on. 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. The legend WRAPS onto as many lines as it needs (0.57.0), so every phase chip on a 20-section plan is visible and clickable; until 0.56.0 it was one line and clipped everything past the pane edge. Below 1150px the chips drop their words and keep the colour dot, as before - **Tag chips in the legend** (0.59.0; rows since 0.60.0). When the plan carries tags, on sections or on rows, the legend leads with a chip per tag (`# Tech 32`: the tag and how many rows choosing it would show, counting the rows in its sections and the rows tagged on their own) and the section chips stand BEHIND them: with no tag chosen they fold away behind one "20 sections" chip (click it to open them all; that is remembered as `prefs.legendSecs`), and with a tag chosen the chips of exactly the sections on screen come back after the tags. A section chip whose name prefix is a chosen tag drops the repeat ("Laser workcells" behind "# Tech"). Choosing tags filters by EFFECTIVE tags: a row's effective tags are its own plus its section's, and a row is shown when ANY of them is chosen. So every row of a chosen section is shown, tagged or not; a row tagged "long-lead" in an untagged section is shown by "long-lead", with its section header above it and the section's other rows hidden; and a row whose own tags do not match, in a section that does not match, is hidden. A section header is shown when the section matches or any of its rows does, and while the filter hides some of its rows its count reads "2 of 3". A delete or a tag edit that takes the last carrier of a chosen tag out of the plan drops it from the filter, and its Ctrl+Z puts the filter back with the tag. This holds in the chart and the table alike; the readouts, Analytics and everything else stay whole-plan. It composes with the section chips (which still switch a shown section off) and with Collapse all (which then counts only the shown sections). **Clear tags** shows everything again. The filter is view state, per person, in `prefs.tagFilter`, so it survives a refresh; a tag the plan no longer has drops out of it. A plan with no tags has no tag chips and no fold, and its legend is what it always was+- **Current in the legend** (0.70.0), between Owner and Overdue: the working set for a review. A row is **current** when it has STARTED (a task's start, a milestone's date: today or earlier) and is not complete or cancelled, so it is the work under way plus the work that should be (a task still "not started" after its start date shows). Future rows and finished ones are hidden. Counts tasks and milestones apart like Overdue (`data-tasks` / `data-ms`); `prefs.currentOnly`; derived on every render and at midnight, never stored. It ANDs with the other filters, so Current plus Overdue shows the overdue rows (every overdue row is current), and it applies to the table too. A month-only date counts from the first of its month; since 0.70.0 a month-only DEADLINE is due at the end of its month for Overdue (before, such a row could never be overdue). - **Owner and Overdue in the legend** (0.64.0), before the tag chips. **Owner** is a select listing every owner on the plan with their row count, then **Unassigned (n)** for the rows nobody owns, because on a young plan that is most of it and it has to be reachable. It is ONE choice shared with the table's owner select (`prefs.ownerFilter`, which replaces the table-only `prefs.tableOwner` and carries an old pick over), so the person picked on the chart is the person the table shows; the chart's copy hides in the table view, where the table's own sits in its toolbar. **Overdue** is a chip with a red dot and a count: a row is overdue when its deadline (a task's end, a milestone's date) is before today and its status does not settle it, complete or cancelled, read through `statusOf` so "completed", "done" and "canceled" settle it too; a blank status is still owed. It is the same rule as the Analytics Overdue card. Every overdue row carries a red edge on its label and a red ring on its bar (a red diamond on a milestone) whether or not the filter is on; clicking the chip shows only those rows (`prefs.overdueOnly`). The chip counts **tasks and milestones apart** (`Overdue 3 | ◆ 2`, also as `data-tasks` / `data-ms`), because milestones usually carry no status and would otherwise pile up as their dates pass and drown the late tasks; a milestone with no status counts as not reached until it is marked complete. **Overdue is derived, never stored:** it is worked out from the dates and statuses on every render, re-read after every edit, at midnight on a tab left open and when the tab comes back into view, and nothing is ever written to the plan, so no `Overdue` tag appears in the data, an export or the changelog, and computing it never makes another open tab stale. Both filters AND with the tags and narrow ROWS, never sections: under either, a section header shows only over rows it still has, its count reads "2 of 5", and Collapse all acts on the sections on screen. Readouts, Analytics and the exports stay whole-plan. An owner who no longer has a row drops out of the filter the way a vanished tag does - **Collapse all / Expand all** leads the legend (0.57.0), in the chart and the table. One press collapses every section to its header; when every section with rows is already collapsed it expands them all. A mixed state collapses. It is the same view state as the per-lane chevrons (`prefs.collapsed`), so it persists across a refresh, follows the chevrons (open the last lane by hand and it reads Collapse all again), and the table agrees with the chart. With everything collapsed the table says so rather than "no items match" - **Click a bar or a milestone** to open the **item editor** (0.65.0), a floating panel beside what you clicked with every field of the row: type, section, name, owner, status, start, deadline, effort, notes, provenance, tags, the label on the bar and the dependencies. It is the ONE editor for a task or a milestone: the pencil on a row, a double-click on a name, and the Table's Deps cell (which focuses "waits on") open the same panel. A single click on a name in the label column renames it in place instead (0.66.1). Everything in it, the dependencies included, is a draft that lands on Apply (or Enter) as one undo, and Cancel or Escape leaves the plan as it was. Through 0.64 a bar click opened a small dates-and-dependencies panel whose dependency changes landed at once, and a milestone click a name-date-description panel; both are gone
docs/BUILD-NOTES.md+26
@@ -3,6 +3,32 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-10-22 (0.70.0, the Current filter)++Drew: "a filter for tasks that only shows tasks that have started prior to today and are+not completed. So if I'm reviewing with the team, I want to see outstanding/current tasks+but not future/completed tasks."++A legend chip next to Overdue, built the same way: one predicate (isCurrent) ANDed into+rowShown and phaseShown, so the chart, the table, the "2 of 5" lane counts and Collapse+all follow it with no code of their own; a pref (currentOnly); a count of tasks and+milestones apart; derived on every render and at midnight, never written to the plan.++The rule: started (a task's start, a milestone's date, today or earlier; "prior to today"+read as including today, since a task that starts today is under way at the review) and+not settled (complete, cancelled), the same settled set as Overdue. A task left "not+started" past its start date is current: that is the row a review most needs. Cancelled+rows are hidden, being neither outstanding nor future. Every overdue row is current, so+the two chips together show the overdue ones.++Found while writing it: isOverdue read a month-only deadline ("2026-10") as no date, so+such a row could never be overdue. Both rules now resolve month-only dates through+spanDay (the start counts from the first of the month, a deadline from the last).++Tests: five checks in the owner-and-overdue flow against an oracle computed from the+plan (the count, the rows on the chart, the same rows in the table, Current plus Overdue,+off again); the chip is in the coverage manifest as a safe toggle.+ ## Build notes, 2026-10-14 (0.69.0, every list walks with the arrow keys)  Drew, with a picture of the status pill's list open on the chart: "all dropdown menus
gantt.html+52−7
@@ -1423,6 +1423,16 @@ a.prov-chip:hover { border-color: var(--accent); background: rgba(0,184,177,0.08 .legend-item.overdue-chip.on { color: var(--text); border-color: var(--alarm-edge); background: var(--alarm-halo); } .legend-item.overdue-chip.none .od-dot { background: var(--text3); } .legend-item.overdue-chip.none .od-n { color: var(--text3); }+/* The Current chip (0.70.0): what has started and is not done, the working set for a+   review. In the accent, not the alarm: it is a lens, not a warning. */+.legend-item.current-chip { background: none; border: 1px solid var(--border); font-family: 'Satoshi', sans-serif; color: var(--text2); padding: 2px 8px; gap: 6px; }+.legend-item.current-chip .cu-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }+.legend-item.current-chip .cu-n, .legend-item.current-chip .cu-ms { font-family: 'JetBrains Mono', monospace; font-size: 0.9em; color: var(--accent); }+.legend-item.current-chip .cu-ms { padding-left: 6px; border-left: 1px solid var(--border); }+.legend-item.current-chip:hover { color: var(--text); border-color: var(--accent); background: var(--row-hover); }+.legend-item.current-chip.on { color: var(--text); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }+.legend-item.current-chip.none .cu-dot { background: var(--text3); }+.legend-item.current-chip.none .cu-n, .legend-item.current-chip.none .cu-ms { color: var(--text3); } /* An overdue row (0.64.0): a red edge on its label and a red ring on its bar, so it shows    without the filter on. Derived each render, never stored. */ .label-row.is-overdue { box-shadow: inset 3px 0 0 var(--alarm); }@@ -4449,7 +4459,7 @@ function tagSigNow(){ return JSON.stringify(planTags().map(e=>[e.tag,e.n,e.items // than vanishing from under the hand that just edited it. function refreshViewChips(){   if(!overdueChip.isConnected)return;-  syncOverdueChip(); syncOwnerPick();+  syncOverdueChip(); syncCurrentChip(); syncOwnerPick();   items.forEach(r=>{ if(isGroup(r))return; const od=isOverdue(r);     if(r._lDiv)r._lDiv.classList.toggle('is-overdue',od); if(r._bDiv)r._bDiv.classList.toggle('is-overdue',od); }); }@@ -4920,6 +4930,7 @@ prefs.legendSecs=prefs.legendSecs===true; if(typeof prefs.ownerFilter!=='string')prefs.ownerFilter=typeof prefs.tableOwner==='string'?prefs.tableOwner:''; delete prefs.tableOwner; prefs.overdueOnly=prefs.overdueOnly===true;+prefs.currentOnly=prefs.currentOnly===true; } let tagsReady=false;             // set once the sidecar is in: before that the data file's tags are not the plan's function tagFilterKeys(){ return new Set(prefs.tagFilter.map(tagKey)); }@@ -4961,17 +4972,35 @@ function ownerPass(r){ function isOverdue(r){   if(!r||r.type==='group')return false;   const dl=r.type==='milestone'?(r.date||''):(r.end||'');-  if(!dl)return false;+  if(!isDateStr(dl))return false;   const st=statusOf(r);-  return st!=='complete'&&st!=='cancelled'&&dayNum(dl)<dayNum(anToday());+  // A month-only deadline ("2026-10") is due at the END of its month (0.70.0: before, it+  // read as no date at all and such a row could never be overdue).+  return st!=='complete'&&st!=='cancelled'&&dayNum(spanDay(dl,true))<dayNum(anToday()); } function overdueRows(){ return items.filter(isOverdue); }+// CURRENT (0.70.0): the working set for a review. A row is current when it has STARTED+// (a task's start, a milestone's date: today or earlier) and its status does not settle+// it, the same settled set as Overdue (complete, cancelled). So it is the work under way+// plus the work that should be: a task still "not started" a week after its start date+// is exactly what a review wants on screen. Future rows and finished ones drop out. Every+// overdue row is current, so the two chips together show the overdue ones. Derived like+// Overdue, never stored; a month-only start counts from the first of its month.+function isCurrent(r){+  if(!r||r.type==='group')return false;+  const s=r.type==='milestone'?r.date:r.start;+  if(!isDateStr(s))return false;+  const st=statusOf(r);+  return st!=='complete'&&st!=='cancelled'&&dayNum(spanDay(s,false))<=dayNum(anToday());+}+function currentRows(){ return items.filter(isCurrent); } // Is ANY view filter hiding rows? Callers that used to ask only about tags ("2 of 5" on // a lane, Collapse all acting on what is on screen) ask this now.-function viewFilterOn(){ return tagFilterOn()||ownerFilterOn()||prefs.overdueOnly; }+function viewFilterOn(){ return tagFilterOn()||ownerFilterOn()||prefs.overdueOnly||prefs.currentOnly; } function rowShown(r,ks){   if(!ownerPass(r))return false;   if(prefs.overdueOnly&&!isOverdue(r))return false;+  if(prefs.currentOnly&&!isCurrent(r))return false;   if(!prefs.tagFilter.length)return true;   ks=ks||tagFilterKeys();   return effTags(r).some(t=>ks.has(tagKey(t)));@@ -4979,7 +5008,7 @@ function rowShown(r,ks){ function phaseShown(i,ks){   // Owner and Overdue choose ROWS, never a section: under either, a header shows only   // over rows it still has on screen.-  if(ownerFilterOn()||prefs.overdueOnly)return items.some(r=>r.phase===i&&!isGroup(r)&&rowShown(r,ks));+  if(ownerFilterOn()||prefs.overdueOnly||prefs.currentOnly)return items.some(r=>r.phase===i&&!isGroup(r)&&rowShown(r,ks));   if(!prefs.tagFilter.length)return true;   const p=phases[i]; if(!p)return true;   ks=ks||tagFilterKeys();@@ -5020,7 +5049,7 @@ const secToggle=document.createElement('button'), tagClear=document.createElemen // Owner and Overdue lead the legend, before the tags (0.64.0). The owner pick is a select // because a plan has a dozen people, not four; it is the chart's copy of the table's own // select, so it hides in the table view, where that one sits in the toolbar.-const ownerPick=document.createElement('div'), ownerPickSel=document.createElement('select'), overdueChip=document.createElement('button');+const ownerPick=document.createElement('div'), ownerPickSel=document.createElement('select'), overdueChip=document.createElement('button'), currentChip=document.createElement('button'); function wireLegendOwner(){ ownerPick.className='legend-item owner-pick gantt-only'; ownerPick.innerHTML='<span class="op-label">Owner</span>';@@ -5030,6 +5059,8 @@ ownerPick.appendChild(ownerPickSel); ownerPickSel.addEventListener('change',()=>setOwnerFilter(ownerPickSel.value)); overdueChip.type='button'; overdueChip.id='overdueChip'; overdueChip.className='legend-item overdue-chip'; overdueChip.addEventListener('click',()=>{ prefs.overdueOnly=!prefs.overdueOnly; savePrefs(); applyTagView(); });+currentChip.type='button'; currentChip.id='currentChip'; currentChip.className='legend-item current-chip';+currentChip.addEventListener('click',()=>{ prefs.currentOnly=!prefs.currentOnly; savePrefs(); applyTagView(); }); } function setOwnerFilter(v){   prefs.ownerFilter=v||''; savePrefs();@@ -5067,6 +5098,18 @@ function syncOverdueChip(){     +(ms?'A milestone with no status counts as not reached: mark it complete once it is. ':'')     +'Worked out from the dates and statuses each time the page draws, and again at midnight; nothing is written to the plan.'; }+// The same two counts as Overdue, for the same reason.+function syncCurrentChip(){+  const cu=currentRows(), ms=cu.filter(r=>r.type==='milestone').length, tk=cu.length-ms, on=prefs.currentOnly;+  currentChip.classList.toggle('on',on); currentChip.classList.toggle('none',!cu.length);+  currentChip.setAttribute('aria-pressed',on?'true':'false');+  currentChip.dataset.tasks=tk; currentChip.dataset.ms=ms;+  currentChip.innerHTML='<span class="cu-dot" aria-hidden="true"></span><span class="cu-label">Current</span><span class="cu-n">'+tk+'</span>'+    +(ms?'<span class="cu-ms" aria-label="'+plural(ms,'milestone')+'">\u25C6 '+ms+'</span>':'');+  currentChip.title=(on?'Show every row again. ':'Show only what is current: ')+    +plural(tk,'task')+(ms?' and '+plural(ms,'milestone'):'')+' that started on or before today ('+fmt(anToday())+') and are not complete or cancelled. '+    +'Future and finished rows are hidden; with Overdue on too, only the overdue ones. Your view only, it does not change the plan.';+} // The page can be left open overnight. When the date turns, what is overdue changes with // nobody touching the plan, so the chip and the marks are re-read then, and whenever the // tab comes back into view.@@ -5116,10 +5159,11 @@ function syncSecToggle(){ } function rebuildLegend(){   legendEl.querySelectorAll('.phase-chip,.tag-chip').forEach(el=>el.remove());-  [secToggle,tagClear,ownerPick,overdueChip].forEach(el=>{ if(el.isConnected)el.remove(); });+  [secToggle,tagClear,ownerPick,currentChip,overdueChip].forEach(el=>{ if(el.isConnected)el.remove(); });   const first=collapseAllBtn.isConnected?collapseAllBtn.nextSibling:legendEl.firstChild;   const put=el=>legendEl.insertBefore(el,first);   syncOwnerPick(); put(ownerPick);+  syncCurrentChip(); put(currentChip);   syncOverdueChip(); put(overdueChip);   const tags=usedTags(), ks=tagFilterKeys(), filtering=tags.length>0&&tagFilterOn();   tags.forEach(e=>put(tagChip(e,ks.has(e.key))));@@ -11011,6 +11055,7 @@ function renderTable(){     // would send the reader looking at the filters for a cause that is not there.     const why=allLanesCollapsed()?'Every section is collapsed. Expand all, at the start of the legend, to see the rows.'       :prefs.overdueOnly&&!items.some(r=>!isGroup(r)&&rowShown(r))?'Nothing is overdue'+(ownerFilterOn()||tagFilterOn()?' for this owner and these tags.':'.')+' Click Overdue in the legend to show everything again.'+      :prefs.currentOnly&&!items.some(r=>!isGroup(r)&&rowShown(r))?'Nothing is current'+(ownerFilterOn()||tagFilterOn()?' for this owner and these tags.':'.')+' Click Current in the legend to show everything again.'       :ownerFilterOn()&&!items.some(r=>!isGroup(r)&&rowShown(r))?'This owner has no rows'+(tagFilterOn()?' with these tags':'')+'. Pick another owner, or All owners.'       :tagFilterOn()&&!items.some(r=>!isGroup(r)&&rowShown(r))?'No row carries these tags, on itself or through its section. Pick other tags in the legend, or clear them.'       :'No items match these filters.';
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.69.0",+  "version": "0.70.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, pushing work LATER only so a lead-time gap you set on purpose is never closed, and one more lights up the critical path, meaning the longest run of work inside every section at once. Cancelled work is left out of both: it never moves, it constrains nothing and it can never glow. Dependency arrows are coloured by the phase of the task they come out of, so where a chain starts reads at a glance. 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. Copy and paste rows: Ctrl+C the selected tasks and milestones, Ctrl+V to drop clones directly below a selected row or at the end of the lane you are looking at, with dependencies AMONG the copied rows remapped onto the copies so a duplicated chain is still a chain, links pointing outside it left naming the originals, and the whole paste one undo and one changelog line. Five statuses in lifecycle order, not started, in progress, complete, blocked, cancelled, with cancelled keeping the row and its history on the plan while taking its hours out of every total. 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+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.69.0",+  "version": "0.70.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, pushing work LATER only so a lead-time gap you set on purpose is never closed, and one more lights up the critical path, meaning the longest run of work inside every section at once. Cancelled work is left out of both: it never moves, it constrains nothing and it can never glow. Dependency arrows are coloured by the phase of the task they come out of, so where a chain starts reads at a glance. 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. Copy and paste rows: Ctrl+C the selected tasks and milestones, Ctrl+V to drop clones directly below a selected row or at the end of the lane you are looking at, with dependencies AMONG the copied rows remapped onto the copies so a duplicated chain is still a chain, links pointing outside it left naming the originals, and the whole paste one undo and one changelog line. Five statuses in lifecycle order, not started, in progress, complete, blocked, cancelled, with cancelled keeping the row and its history on the plan while taking its hours out of every total. 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": {
test/ui_test.mjs+17
@@ -1156,6 +1156,7 @@ const MANIFEST = [   { sel: '#ganttOwner', label: 'legend owner filter', terminal: true, by: 'owner and overdue' },   { sel: '.owner-pick', label: 'legend owner filter (its wrapper)', terminal: true, by: 'owner and overdue' },   { sel: '#overdueChip', label: 'legend: overdue only', safe: true, twice: true },+  { sel: '#currentChip', label: 'legend: current only (started, not done)', safe: true, twice: true },   { sel: '.sec-tag-in', label: 'section tag: type to add', terminal: true, by: 'tags' },   { sel: '.sec-tag-x', label: 'section tag: remove one', terminal: true, by: 'tags', key: c => c.ds.tag, pin: ['Launch', 'Tech', 'Process'] },   { sel: '#tagPrefixBtn', label: 'tags from name prefixes', terminal: true, by: 'tags' },@@ -1826,6 +1827,22 @@ async function editorFlows(browser, S) {   check('Overdue on shows only the overdue rows', shown.length === od.length && od.every(r => shown.some(t => t.includes(r.name))), shown.join(' | '));   await page.click('#overdueChip'); await settle(page, 250);   check('Overdue off shows every row again', (await chartNames()).length === live.length);+  // 0.70.0: the Current chip. Current = started (a task's start, a milestone's date, on or+  // before the pinned today) and not complete or cancelled; every overdue row is current.+  const cur = live.filter(r => { const s = r.type === 'milestone' ? r.date : r.start; return s && dayNum(s) <= dayNum(today) && !['complete', 'cancelled'].includes(r.status); });+  const cuMs = cur.filter(r => r.type === 'milestone').length;+  const cchip = await page.$eval('#currentChip', e => ({ tk: Number(e.dataset.tasks), ms: Number(e.dataset.ms) }));+  check(`the Current chip counts what has started and is not done (${cur.length - cuMs} + ${cuMs} of ${live.length})`, cur.length > od.length && cur.length < live.length && cchip.tk === cur.length - cuMs && cchip.ms === cuMs, JSON.stringify(cchip));+  await page.click('#currentChip'); await settle(page, 250);+  shown = await chartNames();+  check('Current on shows exactly those rows: nothing in the future, nothing finished', shown.length === cur.length && cur.every(r => shown.some(t => t.includes(r.name))), shown.join(' | '));+  await view(page, 'table');+  check('the table shows the same rows under Current', (await tableRows()).length === cur.length, String((await tableRows()).length));+  await view(page, 'gantt');+  await page.click('#overdueChip'); await settle(page, 250);+  check('Current and Overdue together show just the overdue rows', (await chartNames()).length === od.length);+  await page.click('#overdueChip'); await settle(page, 150); await page.click('#currentChip'); await settle(page, 250);+  check('Current off shows every row again', (await chartNames()).length === live.length);   // Nothing about either filter is written to the plan: they are one viewer's lens.   const plan2 = await savedPlan(S.url, { minWait: 300 });   check('neither filter writes to the plan (no Overdue tag, rows unchanged)', JSON.stringify(plan2.items) === JSON.stringify(plan1.items) && !plan2.items.some(r => (r.tags || []).some(t => /overdue/i.test(t))));