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.45.0: US federal holidays are non-working days, merged per-person card, remaining and average hours
6 files changed
+397−67
README.md+5−5@@ -4,11 +4,11 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau ## Features -**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 or a Sunday. 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 BUSINESS days, so a five-day task laid across a weekend 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 to the next business 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.+**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 business days, 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.** **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. -**Analytics.** A third lens, next to Gantt and Table. Total effort for the plan in hours (business days times eight, a forty-hour week), hours per person with shares and the unassigned bucket, status counts overall and per person, 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.+**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. **Three lenses over one plan.** A **Gantt / Table / Analytics** toggle in the header. The chart answers "when". The table answers "whose, and by when": every task and every milestone as a row with Owner, Start, Deadline, Status and Notes, sortable on any column, filtered to one person with the owner dropdown, and editable in place. An edit made in the table is already true on the chart when you toggle back, because there is only one set of items underneath. The choice of lens persists, and `project.view.mode` can pin it for a plan that is an assignment list more than a timeline. @@ -74,7 +74,7 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau **Fit to the pane.** One click on Fit picks the finest cell size that shows the whole plan without scrolling sideways, then stretches the cells to fill the width exactly. Click it again after resizing the window to re-fit. -**Cell size.** Days, Weeks, Months, Quarters or Years. Each grain carries its own minimum cell width, sized to the widest label it draws, so a fine grain scrolls rather than squashing and a coarse one stretches to fill. Days adds weekend shading and a full calendar picker.+**Cell size.** Days, Weeks, Months, Quarters or Years. Each grain carries its own minimum cell width, sized to the widest label it draws, so a fine grain scrolls rather than squashing and a coarse one stretches to fill. Days adds weekend shading, a differently coloured band on every US federal holiday with its name in a tooltip, and a full calendar picker. **Editable milestones.** Drag a milestone sideways to move its date, or click it to edit its name, date and description. Line style is the default: the tag on the dashed rule is the handle and the milestone's own row is hidden, since the rule already spans the chart. Long titles wrap, and a tag that would still collide with its neighbour drops to a second row. Diamond style keeps the row. @@ -262,7 +262,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. One undo. Disabled when the plan has no dependencies |+| 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 | | A- / A+ | Step the text size for both views. Click the percentage to reset | | Hover "today" | The current date |
SKILL.md+37−10@@ -6,8 +6,9 @@ description: > and dependency arrows. Features: click-to-filter legend, drag-to-resize bars, inline date editor, dependency management, undo (Ctrl+Z), and auto-save to a sidecar state file. One button auto-schedules the whole- plan so every dependency lines up finish-to-start on business days with- no weekend starts or ends, and another lights up the critical path.+ 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. 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,@@ -217,7 +218,7 @@ rather than a category. - **Text size** A-minus/A+ in the header: seven steps from 85% to 150%, applies to the chart and the table, persists in `localStorage` as `prefs.fs`. Row and bar geometry scale with the type - **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 or a Sunday. 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 if it was on one), because that is the one thing in a plan this cannot derive. Duration is preserved in BUSINESS days, not calendar days, so a five-day task laid across a weekend 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 to the next business 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+- **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 - **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@@ -314,24 +315,50 @@ The third lens, and the one to reach for when the question is "how much" rather same `items` every time the lens is entered or an edit lands, so it can never disagree with the other two. -**The hour rule.** A task's effort is the BUSINESS days in its span, counting both ends,+**The hour rule.** A task's effort is the WORKING days in its span, counting both ends, times eight. A forty-hour week, so Monday to Friday is 40h and the same task dragged-across a weekend to the Tuesday is 56h. A milestone is a date rather than work and-carries no hours; the owner table counts milestones separately as `+nM`.+across a weekend to the Tuesday is 56h. A US federal holiday is not a working day, so a+task spanning Labor Day is 8h lighter than its calendar span suggests. A milestone is a+date rather than work and carries no hours; the owner table counts milestones separately+as `+nM`. -Eight cards:+Seven cards: | Card | What it answers | |------|-----------------|-| Project total | Total effort in hours, business days in the plan's span, dated tasks, milestones, people |-| Hours per person | Every owner, unassigned included, with hours, share of the total and item counts, sorted by hours |+| Project total | Total effort, WORKING HOURS LEFT to the project end, AVERAGE per person assigned, working days in the plan's span, dated tasks, milestones, people |+| Per person | One row per owner, unassigned included: hours, share of the total and item counts FIRST, then the four status counts. Sorted by hours | | Status | The four status counts over every task and milestone, with shares |-| Status per person | The same four counts, one row per owner | | Per section | Items, hours and the date range each section actually covers, derived from its rows | | Milestones | Every dated milestone with its date, days from today and owner | | Dependencies | Link count, rows in the graph, rows outside it, and a cycle warning naming the rows | | Overdue | Every row with a deadline in the past and a status that is not complete | +**Working hours left** is calendar capacity rather than a task sum: the working days from+today to the last dated row in the plan, at 8h each. Today counts when today is itself a+working day, so a plan opened on a Monday morning still has that Monday in front of it. A+plan whose end has passed reads zero.++**Average per person assigned** divides the hours that have an owner by the number of+named owners carrying any. The unassigned bucket is in neither the numerator nor the+divisor, and the subtitle says how many people the divisor was.++**US federal holidays are non-working days.** All eleven statutory holidays of 5 U.S.C.+6103, observed: the six pinned to a weekday (MLK, Washington, Memorial, Labor, Columbus,+Thanksgiving) and the five fixed dates (New Year, Juneteenth, Independence, Veterans,+Christmas), each moved to the Friday before when it falls on a Saturday and the Monday+after when it falls on a Sunday. They are computed for whatever years the plan covers, not+read from a list, and the observance shift crosses the year boundary: New Year's Day 2028+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+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.++**Re-running Auto-schedule on a plan written before 0.45.0 may move dates around a+holiday. That is intended.**+ It is WHOLE-PLAN by construction, which is why the legend and the filters are hidden with it: a total that quietly excluded a filtered lane would be a wrong total presented as a right one. It renders in both themes and prints.
docs/BUILD-NOTES.md+135@@ -3,6 +3,141 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-19 (0.45.0, federal holidays, merged per-person card, remaining hours)++Three changes from Drew, all in the Analytics lens and the calendar. `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.** The+suites run on synthetic fixtures on their own ports.++> ### READ THIS FIRST: US federal holidays are now NON-WORKING DAYS+>+> A US federal holiday now counts exactly like a Saturday, everywhere in the app that+> counts business days. That is Auto-schedule, the critical path, and every hours figure+> in Analytics.+>+> **Re-running Auto-schedule on an existing plan may move dates.** A task whose span+> covers Labor Day is now one working day shorter, or one calendar day longer, depending+> on which end is pinned; a task that started or ended on a holiday is pushed off it. This+> is intended. Nothing moves on its own: the plan on disk is untouched until somebody+> presses the button.+>+> The motivating case is Drew's own window. Labor Day 2026 is Monday 7 September, and it+> sits in the middle of it.++### 3. Federal holidays, computed rather than listed++`fedHolidays(y)` returns a Map of observed date to holiday name for any year, built from+the rules in 5 U.S.C. 6103 and the OPM observance rule. There is no hardcoded list of+dates for one year, because a plan that runs into 2027 would silently lose them.++Six of the eleven are pinned to a weekday and never move: MLK is the third Monday in+January, Washington's Birthday the third Monday in February, Memorial the last Monday in+May, Labor the first Monday in September, Columbus the second Monday in October, and+Thanksgiving the fourth Thursday in November. The other five are fixed calendar dates and+shift when they land on a weekend: a Saturday is observed on the Friday before, a Sunday+on the Monday after.++**The shift crosses the year boundary in both directions, and both are real.** New Year's+Day 2028 is a Saturday, so federal employees take Friday 31 December 2027. New Year's Day+2023 was a Sunday, so they took Monday 2 January. `fedHolidays(y)` therefore builds the+candidates for `y` **and** the New Year of `y+1`, then keeps only what actually lands in+`y`. Every observed day belongs to exactly one year's set no matter which way it moved,+which is why a calendar year can carry ten, eleven or twelve of them. 2022 carried ten.++**Every observed date is a weekday by construction.** That is not a coincidence worth+relying on by accident, so it is the thing the whole design leans on: the holiday set and+the weekend cannot overlap, which lets `isOffDay` be a plain OR and lets `bizBetween`+subtract a flat holiday count without asking whether it had already skipped one.++One Map per year, memoised in `HOLIDAY_CACHE`. A plan spans a handful of years, so this is+a handful of Maps for the life of the tab.++#### The arithmetic, in one place++The change is deliberately concentrated. `isOffDay(ds)` is the weekend OR a holiday, and+`nextBiz`, `prevBiz`, `addBiz` and `bizBetween` now ask it instead of `isWeekend`. Nothing+else in the app changed, because nothing else in the app does business-day arithmetic:+Auto-schedule, CPM and `taskHours` are all built out of those four functions. `isWeekend`+survives untouched for the places that genuinely mean Saturday and Sunday.++`bizBetween` keeps its closed-form week count and then hands back the holidays inside the+span, one Map lookup per year touched, so a ten-year span still costs six comparisons plus+a dozen per year rather than a walk.++The `nextBiz`/`prevBiz` guard went from 7 to 10. The longest run of consecutive+non-working days the rules can produce is three, a Friday-observed holiday and the weekend+after it.++#### On the chart++A holiday column gets a weekend-style band in a **different colour**: `--holiday` is warm+where `--weekend` is neutral, in both themes and in print. The band carries a `title`+naming the holiday, and because the grid layer sits under the bars and takes no pointer,+the tooltip a reader can actually reach is on the header cell above it, which is also+shaded. A static `US holiday` key sits in the legend.++Bands render at day grain only, exactly as weekend bands always have. At week, month and+quarter grain there are no bands of either kind, and the legend key hides with them.++**One rendering change beyond the holidays:** the day-grain grid used to draw the month+boundary rule *instead of* the band when a non-working day fell on the first of a month.+Bands and rules are now two separate passes, so a column can have both. New Year's Day is+always the first of the month, and shading it is the point.++### 2. The project-total card: remaining hours and an average++**Working hours left to project end** is calendar capacity, not a task sum: the working+days from today to the last dated row, one person-shift wide, at 8h each. A plan whose+task hours exceed it needs more than one person on it.++**Today counts when today is a working day.** A plan opened on a Monday morning still has+that Monday's eight hours in front of it. `bizBetween` counts both ends and drops today+again when today is a Saturday or a holiday, so the rule needs no special case. A plan+whose end has passed reads zero.++**Average per person assigned** divides the hours that have an owner by the number of+named owners carrying any. The unassigned bucket is in **neither** half of it: counting it+as a person would invent a teammate, and leaving its hours in the numerator while dropping+it from the divisor would inflate everybody. The subtitle says so, and says how many+people the divisor was.++### 1. One "Per person" card instead of two++`Hours per person` and `Status per person` listed the same owners in the same order on two+separate cards. Reading them meant finding the same name twice and holding the first half+in your head, which is the comparison a table exists to do for you.++One full-width card now: owner, share bar, hours, %, items, then the four status counts.+Hours first, because that is what the card is sorted by. The unassigned bucket is still a+row, the total row now carries the status totals as well, and the `+nM` milestone notation+is unchanged.++The status totals on that row are summed over the **displayed** rows, so the column adds+up to what is above it. The separate `Status` card still counts every row in the plan.++### What the suites had to change++Two files, and only two. The scan across every standing suite found the rest untouched:+they carry holiday-crossing spans, but nothing in them recomputes a date.++- **`g42/v42.mjs`** and **`g43/v43.mjs`** grew their own holiday model, walked rather than+ computed, so the suite still cannot agree with the app by sharing its arithmetic. Their+ `NEXTBIZ` and `BIZ` now ask `OFF` instead of `WK`.+- **`v42.mjs`** re-baselined five literals, each one a holiday landing in the fixture:+ `A4 straddle` 01-16/01-22 becomes 01-16/01-23 (MLK, 19 January 2026); `A6 gate` 01-22+ becomes 01-23 following it; `A7 sunday` 02-16 becomes 02-17, because the Monday a Sunday+ milestone was nudged to is Washington's Birthday; the same A4 literal again in the+ idempotence re-check; and the Auto-schedule tooltip, which now says "skipping weekends+ and US federal holidays".+- Both suites' "no task starts or ends on a Saturday or a Sunday" invariant became "on a+ weekend or a federal holiday", which is strictly stronger.++The 0.44.0 against 0.45.0 A/B was run on two fixtures on purpose. On the holiday-free `g42`+fixture the merged card's values are **byte-identical** to the two cards it replaces,+which isolates the merge from the calendar. On the Labor Day `v45` fixture the two builds+differ by exactly 16h, which is the two tasks that span 7 September and nothing else.+ ## Build notes, 2026-08-19 (0.44.0, Alt+Arrow: shut lanes open, scattered selections group) Two changes from Drew, both about moving rows with Alt+Up and Alt+Down. `gantt.html`
gantt.html+203−43@@ -70,6 +70,10 @@ --on-accent: #05221f; --grid-line: rgba(48,54,61,0.18); --weekend: rgba(255,255,255,0.022);+ /* A federal holiday is a non-working day like the weekend, so it gets the same+ kind of band, warmed and lifted enough that the two read as different bands+ at a glance rather than as one shade of grey. */+ --holiday: rgba(255,176,102,0.085); --row-hover: rgba(255,255,255,0.04); /* Phase ramp: teal -> purple -> blue, then a lighter step of each. Used when a phase declares no explicit colour. */@@ -102,6 +106,7 @@ --on-accent: #ffffff; --grid-line: rgba(31,35,40,0.10); --weekend: rgba(31,35,40,0.04);+ --holiday: rgba(199,105,10,0.105); --row-hover: rgba(31,35,40,0.04); /* Deepened for paper: each clears 3:1 on white while holding its hue. */ --ph1: #00a49d; --ph2: #8c6bf7; --ph3: #3b95ff;@@ -113,7 +118,7 @@ :root { --bg:#ffffff; --surface:#ffffff; --elevated:#ffffff; --overlay:#ffffff; --border:#d0d7de; --border-muted:#d8dee4; --text:#1f2328; --text-body:#24292f; --text2:#59636e; --text3:#8c959f;- --accent:#00857f; --today:var(--text2); --grid-line:rgba(31,35,40,0.10); --weekend:rgba(31,35,40,0.04);+ --accent:#00857f; --today:var(--text2); --grid-line:rgba(31,35,40,0.10); --weekend:rgba(31,35,40,0.04); --holiday:rgba(199,105,10,0.105); --ph1:#00a49d; --ph2:#8c6bf7; --ph3:#3b95ff; --sig:#b45309; --sig-edge:#ffffff; --crit:#4c1d95; --crit-glow:rgba(76,29,149,0.95); --crit-halo:rgba(76,29,149,0.40);@@ -262,6 +267,10 @@ body.col-resizing { cursor: col-resize; user-select: none; } .q-row { height: 22px; } .m-row { height: 24px; } .q-cell { display: flex; align-items: center; justify-content: center; height: 100%; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); border-bottom: 1px solid var(--border-muted); border-right: 1px solid var(--border-muted); } .m-cell.weekend { color: var(--text3); }+/* The header cell is the one part of a non-working column a pointer can actually+ reach: the bands themselves sit in the grid layer under the bars and are+ pointer-events:none, so the title that names the holiday lives here. */+.m-cell.holiday { color: var(--text2); background: var(--holiday); cursor: help; } .m-cell { display: flex; align-items: center; justify-content: center; height: 100%; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); border-bottom: 1px solid var(--border); border-right: 1px solid var(--border-muted); } .label-h-q { height: 22px; border-bottom: 1px solid var(--border-muted); } .label-h-m { height: 24px; border-bottom: 1px solid var(--border); }@@ -721,9 +730,10 @@ table.an-table tr.total td { border-top: 1px solid var(--border); border-bottom: /* A full-width card gives its first column the slack, so a long task name has room to be read instead of the number columns drifting apart across a metre of empty card. */ .an-card.wide table.an-table th:first-child { width: 34%; }-/* Four narrow counts do not need a metre of card, so this one keeps its own width and- stays left-aligned in it. */-table.an-table.tight { width: 760px; max-width: 100%; }+/* The per-person table carries nine columns, five of them narrow counts, so it gives+ the name less of the card than a four-column one can afford to: 34% of a full-width+ card for the owner would squeeze the counts into each other. */+.an-card.wide table.an-table.people th:first-child { width: 20%; } /* The share bar: the row's own cell, filled to its fraction. Painted UNDER the text rather than beside it, so a long name never steals the bar's width. */ .an-bar { position: relative; height: 6px; border-radius: 3px; background: var(--border-muted); overflow: hidden; min-width: 40px; }@@ -1467,27 +1477,112 @@ function addDays(ds, n){const[y,mo,d]=ds.split('-').map(Number);const dt=new Dat function dayNum(ds){const[y,m,d]=String(ds).split('-').map(Number);return Math.round(Date.UTC(y,m-1,d)/86400000);} function dowOf(ds){return ((dayNum(ds)%7)+7+4)%7;} function isWeekend(ds){const w=dowOf(ds);return w===0||w===6;}-// The next business day ON or after this one, and its mirror. Both are identity-// on a weekday, so calling them on a date that is already fine costs one modulo.-function nextBiz(ds){let s=ds,g=0;while(isWeekend(s)&&g++<7)s=addDays(s,1);return s;}-function prevBiz(ds){let s=ds,g=0;while(isWeekend(s)&&g++<7)s=addDays(s,-1);return s;}-// Business days in [a,b], counting both ends, which is how a person reads a task++// ── US federal holidays ──+//+// The eleven statutory holidays of 5 U.S.C. 6103, as OBSERVED, which is the only+// form worth computing here: a plan is scheduled against the days people are at+// work, and nobody is at work on the Friday the Fourth of July was moved to. Six+// are already pinned to a Monday or a Thursday and never move; the other five are+// fixed calendar dates and follow the OPM rule, a Saturday observed on the Friday+// before and a Sunday on the Monday after.+//+// Because every observed date is a weekday by construction, the holiday set and the+// weekend cannot overlap, which is what lets isOffDay below be a plain OR and lets+// bizBetween subtract a flat count without asking whether it already skipped one.+//+// The shift crosses the year boundary in BOTH directions and both are real: New+// Year's Day 2028 is a Saturday, so it is observed on Friday 31 December 2027, and+// New Year's Day 2023 was a Sunday, observed on Monday 2 January. fedHolidays(y)+// therefore builds the candidates for y AND the New Year of y+1, then keeps only+// what actually lands in y, so every observed day belongs to exactly one year's set+// no matter which way it moved. A calendar year can hold ten, eleven or twelve of+// them for that reason, and 2022 really did hold ten.+function pad2(n){return String(n).padStart(2,'0');}+// The nth given weekday of a month, 1-based, in the getDay() convention: 0 Sunday,+// 1 Monday, 4 Thursday.+function nthDow(y,m,dow,n){+ const first=`${y}-${pad2(m)}-01`;+ return addDays(first,((dow-dowOf(first))+7)%7+(n-1)*7);+}+// The last given weekday of a month, counted back from its final day, so May with+// five Mondays gives the fifth and May with four gives the fourth.+function lastDow(y,m,dow){+ const last=`${y}-${pad2(m)}-${pad2(new Date(y,m,0).getDate())}`;+ return addDays(last,-(((dowOf(last)-dow)+7)%7));+}+function observedDay(ds){+ const w=dowOf(ds);+ return w===6?addDays(ds,-1):w===0?addDays(ds,1):ds;+}+// One Map per year, date string to holiday name, built once and kept. A plan spans+// a handful of years, so this is a handful of Maps for the life of the tab.+const HOLIDAY_CACHE=new Map();+function fedHolidays(y){+ let s=HOLIDAY_CACHE.get(y);+ if(s)return s;+ const fx=(m,d)=>observedDay(`${y}-${pad2(m)}-${pad2(d)}`);+ s=new Map();+ [["New Year's Day",fx(1,1)],+ ['Martin Luther King, Jr. Day',nthDow(y,1,1,3)],+ ["Washington's Birthday",nthDow(y,2,1,3)],+ ['Memorial Day',lastDow(y,5,1)],+ ['Juneteenth National Independence Day',fx(6,19)],+ ['Independence Day',fx(7,4)],+ ['Labor Day',nthDow(y,9,1,1)],+ ['Columbus Day',nthDow(y,10,1,2)],+ ['Veterans Day',fx(11,11)],+ ['Thanksgiving Day',nthDow(y,11,4,4)],+ ['Christmas Day',fx(12,25)],+ // Next year's New Year, which is observed on 31 December of THIS year whenever it+ // falls on a Saturday. In every other year the filter below drops it again.+ ["New Year's Day",observedDay(`${y+1}-01-01`)]+ ].forEach(([n,d])=>{ if(+d.slice(0,4)===y)s.set(d,n); });+ HOLIDAY_CACHE.set(y,s);+ return s;+}+function isHoliday(ds){ return fedHolidays(+String(ds).slice(0,4)).has(String(ds)); }+function holidayName(ds){ return fedHolidays(+String(ds).slice(0,4)).get(String(ds))||''; }+// A day nobody works: the weekend or an observed federal holiday. Everything below+// this line asks THIS and not isWeekend, which is what makes a holiday behave+// exactly like a Saturday everywhere in the app at once.+function isOffDay(ds){ return isWeekend(ds)||isHoliday(ds); }++// The next working day ON or after this one, and its mirror. Both are identity on a+// working day, so calling them on a date that is already fine costs one modulo and+// one Map hit. The longest run of consecutive non-working days the rules can produce+// is three (a Friday-observed holiday and the weekend after it), so the guard has+// room to spare.+function nextBiz(ds){let s=ds,g=0;while(isOffDay(s)&&g++<10)s=addDays(s,1);return s;}+function prevBiz(ds){let s=ds,g=0;while(isOffDay(s)&&g++<10)s=addDays(s,-1);return s;}+// Working days in [a,b], counting both ends, which is how a person reads a task // that starts and ends on the same Monday: one day, not zero. Whole weeks are // five days each and only the remainder is walked, so a ten-year span costs the-// same six comparisons a ten-day one does.+// same six comparisons a ten-day one does; the holidays inside the span are then+// handed back, one Map per year touched. function bizBetween(a,b){ const A=dayNum(a),B=dayNum(b); if(B<A)return 0; const total=B-A+1,weeks=Math.floor(total/7); let c=weeks*5; for(let i=weeks*7;i<total;i++){const w=((A+i)%7+7+4)%7;if(w!==0&&w!==6)c++;}- return c;+ return c-holidaysBetween(a,b); }-// n business days forward from a business day, counting `ds` as the first, so+// Observed holidays in [a,b]. Every one of them is a weekday, so each is a day the+// weekday count above has already added and now has to give back.+function holidaysBetween(a,b){+ const A=dayNum(a),B=dayNum(b);+ if(B<A)return 0;+ let n=0;+ for(let y=+String(a).slice(0,4);y<=+String(b).slice(0,4);y++)+ fedHolidays(y).forEach((_,d)=>{const k=dayNum(d);if(k>=A&&k<=B)n++;});+ return n;+}+// n working days forward from a working day, counting `ds` as the first, so // addBiz(d,1) === d and addBiz(Friday,2) === the following Monday. function addBiz(ds,n){ let s=nextBiz(ds),k=Math.max(1,Math.round(n));- while(k>1){s=addDays(s,1);if(!isWeekend(s))k--;}+ while(k>1){s=addDays(s,1);if(!isOffDay(s))k--;} return s; } @@ -1527,7 +1622,7 @@ $('hdr').innerHTML = ` <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="6" r="2"/><circle cx="19" cy="18" r="2"/><path d="M7 6h6a3 3 0 0 1 3 3v7"/><path d="M14 14l2 3 2-3"/></svg><span class="btn-label">Deps</span></button> </div> <div class="plan-ctl">- <button class="act-btn" id="btnAuto" title="Align all task dates so dependencies line up finish-to-start, skipping weekends">+ <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>@@ -2935,6 +3030,18 @@ function rebuildLegend(){ const first=legendEl.firstChild; phases.forEach((ph,i)=>legendEl.insertBefore(phaseChip(ph,i),first)); }+// A key for the second band colour, and nothing more: the holidays are not something+// a reader can switch off, because the schedule underneath them is computed with them+// in it. It is static for that reason, and it appears only at the grain where the+// bands themselves are drawn, which holidayLegendGrain() is told by buildGridLines.+const holL=document.createElement('div');+function buildLegendHoliday(){+holL.className='legend-item static gantt-only';+holL.innerHTML='<div class="legend-dot" style="background:var(--holiday);box-shadow:inset 0 0 0 1px var(--border)"></div>US holiday';+holL.title='US federal holidays are shaded like the weekend and count as non-working days everywhere in the plan. Hover a shaded date above the chart to see which one it is.';+legendEl.appendChild(holL);+}+function holidayLegendGrain(dayGrain){ holL.style.display=dayGrain?'':'none'; } const msL=document.createElement('div'); function buildLegendMilestone(){ msL.className='legend-item';msL.innerHTML='<div class="legend-diamond"></div>Milestone';msL.addEventListener('click',()=>{msVis=!msVis;msL.classList.toggle('off',!msVis);applyFilter();});legendEl.appendChild(msL);@@ -2958,6 +3065,7 @@ todayL.addEventListener('click',()=>{ positionToday(); }); legendEl.appendChild(todayL);+buildLegendHoliday(); // The table's filters join the legend line rather than owning a second band. // Done here, after the chips, so they sit to the right of them. legendEl.appendChild($('tableTools'));@@ -3012,6 +3120,13 @@ function buildTimeHeaders(){ const d=unitDate(i); const c=document.createElement('div');c.className='m-cell';c.style.width=COL+'px'; if(g==='day'&&(d.getDay()===0||d.getDay()===6))c.classList.add('weekend');+ // The holiday's name goes on the header cell rather than on the band, because the+ // band is under the bars and takes no pointer. One hover answers "why is that+ // column shaded, and why did Auto-schedule step over it".+ if(g==='day'){+ const hn=holidayName(isoDay(d));+ if(hn){c.classList.add('holiday');c.title=hn+' (US federal holiday, a non-working day)';}+ } c.textContent=botLabel(d); mRow.appendChild(c); }@@ -3019,18 +3134,35 @@ function buildTimeHeaders(){ function buildGridLines(){ gridLayer.innerHTML='';gridEls.length=0; const g=G();+ holidayLegendGrain(g==='day'); if(g==='day'){- // a rule per day is noise: month boundaries only, with weekends shaded+ // A rule per day is noise: month boundaries only, with every NON-WORKING column+ // shaded. Weekends and federal holidays get the same kind of band in two+ // deliberately different colours, so the reader can see at a glance both that a+ // column is closed and which kind of closed it is.+ //+ // Bands are laid down before the boundary rules and in a separate pass, so a+ // holiday that falls on the first of a month gets both. New Year's Day always+ // does, and shading it is the whole point.+ for(let i=0;i<UNITS;i++){+ const d=addD(START,i),ds=isoDay(d);+ const hn=holidayName(ds);+ if(!hn&&!(d.getDay()===0||d.getDay()===6))continue;+ const l=document.createElement('div');+ l.className='gl-band '+(hn?'holiday':'weekend');+ l.style.cssText=`position:absolute;top:0;bottom:0;left:${i*COL}px;width:${COL}px;background:var(--${hn?'holiday':'weekend'});pointer-events:none;`;+ // The band takes no pointer, so this title is for anything reading the DOM+ // rather than for a hover; the header cell above it carries the one a reader+ // can actually reach.+ if(hn)l.title=hn+' (US federal holiday, a non-working day)';+ gridLayer.appendChild(l);gridEls.push(l);+ } for(let i=0;i<=UNITS;i++){ const d=addD(START,i); if(d.getDate()===1||i===0||i===UNITS){ const l=document.createElement('div'); l.style.cssText=`position:absolute;top:0;bottom:0;left:${i*COL}px;width:1px;background:var(--grid-line);pointer-events:none;`; gridLayer.appendChild(l);gridEls.push(l);- } else if(d.getDay()===0||d.getDay()===6){- const l=document.createElement('div');- l.style.cssText=`position:absolute;top:0;bottom:0;left:${i*COL}px;width:${COL}px;background:var(--weekend);pointer-events:none;`;- gridLayer.appendChild(l);gridEls.push(l); } } return;@@ -7756,8 +7888,26 @@ function analyticsModel(){ const g=depGraph(); const cyc=topoOrder(g).stuck; const noDeps=g.nodes.filter(n=>!n.preds.length&&!n.succs.length).length;+ // WORKING HOURS LEFT is a different question from the sum of the tasks, and the+ // more useful one once a plan is under way: it is the calendar capacity between now+ // and the finish, one person-shift wide, so a plan whose task hours exceed it needs+ // more than one person on it. Counted from today INCLUSIVE when today is itself a+ // working day, because a plan opened on a Monday morning still has that Monday's+ // eight hours in front of it; bizBetween does that inclusion and drops today again+ // when today is a Saturday or a federal holiday. A finished plan has none left.+ const remainBiz=last&&dayNum(today)<=dayNum(last)?bizBetween(today,last):0;+ const remainingHours=remainBiz*HOURS_PER_DAY;+ // The average is over the people who actually carry hours. The unassigned bucket is+ // in NEITHER the numerator nor the divisor: counting it as a person would invent a+ // teammate, and leaving its hours in the numerator while dropping it from the+ // divisor would inflate everyone. So this is the mean load of the named people, and+ // the per-person card beside it is where the outliers show.+ const loaded=people.filter(b=>b.owner&&b.hours>0);+ const assignedHours=loaded.reduce((a,b)=>a+b.hours,0);+ const avgHours=loaded.length?Math.round(assignedHours/loaded.length):0; return {rows,tasks,ms:ms.sort((a,b)=>dayNum(a.date)-dayNum(b.date)),totalHours, first,last,spanBiz,people,status,lanes,overdue,+ remainBiz,remainingHours,loaded:loaded.length,assignedHours,avgHours, deps:{edges:g.edges,isolated:noDeps,cycle:cyc},today}; } function renderAnalytics(){@@ -7769,27 +7919,46 @@ function renderAnalytics(){ const stat=(v,u,k)=>'<div class="an-stat"><div class="v">'+esc(v)+(u?'<small>'+esc(u)+'</small>':'')+'</div><div class="k">'+esc(k)+'</div></div>'; const out=[]; - // 1. the plan in five numbers- out.push(card('wide','Project total','Business days only, Monday to Friday, at '+HOURS_PER_DAY+'h a day, so a five day task is '+(HOURS_PER_DAY*5)+'h.',+ // 1. the plan in seven numbers: what it costs, what is left, and how it is spread+ out.push(card('wide','Project total',+ 'Working days only, Monday to Friday and never a US federal holiday, at '+HOURS_PER_DAY+'h a day, so a five day task is '+(HOURS_PER_DAY*5)+'h. '+ +'Hours left is the calendar capacity from today to the last dated row, counting today when today is a working day. '+ +'The average divides the hours that have an owner by the '+m.loaded+' '+(m.loaded===1?'person':'people')+' carrying them, so the unassigned bucket is in neither half of it.', '<div class="an-hero">' +stat(m.totalHours.toLocaleString(),'h','total effort')- +stat(String(m.spanBiz),'business days','calendar span'+(m.first?', '+fmt(m.first)+' to '+fmt(m.last):''))+ +stat(m.remainingHours.toLocaleString(),'h','working hours left to project end'+(m.last?', '+fmt(m.last):''))+ +stat(m.avgHours.toLocaleString(),'h','average per person assigned')+ +stat(String(m.spanBiz),'working days','calendar span'+(m.first?', '+fmt(m.first)+' to '+fmt(m.last):'')) +stat(String(m.tasks.length),'','dated tasks') +stat(String(m.ms.length),'','milestones') +stat(String(m.people.length),'','people, including unassigned') +'</div>')); - // 2. hours per person+ // 2. per person: the load and the state of it, on ONE row each+ //+ // These were two cards, one for hours and one for the four status counts, over the+ // same list of owners in the same order. Reading them meant finding the same name+ // twice and holding the first half in your head, which is exactly the comparison a+ // table exists to do for you. Hours come first because that is what the card is+ // sorted by; the counts follow on the same line.+ const stTot={};+ STATUS_ORDER.forEach(k=>stTot[k]=m.people.reduce((a,b)=>a+(b.status[k]||0),0)); const ph=m.people.map(b=>'<tr>' +'<td class="name'+(b.owner?'':' muted')+'">'+esc(b.owner||'unassigned')+'</td>'- +'<td style="width:26%">'+anBar(pct(b.hours,m.totalHours))+'</td>'+ +'<td style="width:18%">'+anBar(pct(b.hours,m.totalHours))+'</td>' +'<td class="n">'+esc(hrs(b.hours))+'</td>' +'<td class="n muted">'+pct(b.hours,m.totalHours)+'%</td>'- +'<td class="n muted">'+b.tasks+(b.ms?' +'+b.ms+'M':'')+'</td></tr>').join('');- out.push(card('','Hours per person','Every owner in the plan, unassigned included. Milestones are dates rather than work, so they carry no hours and are counted separately as +nM.',- m.people.length?('<table class="an-table"><thead><tr><th>Owner</th><th>Share</th><th class="n">Hours</th><th class="n">%</th><th class="n">Items</th></tr></thead><tbody>'- +ph+'<tr class="total"><td>Total</td><td></td><td class="n">'+esc(hrs(m.totalHours))+'</td><td class="n">100%</td><td class="n">'+m.tasks.length+(m.ms.length?' +'+m.ms.length+'M':'')+'</td></tr>'- +'</tbody></table>'):'<div class="an-empty">Nothing in this plan carries dates yet.</div>'));+ +'<td class="n muted">'+b.tasks+(b.ms?' +'+b.ms+'M':'')+'</td>'+ +STATUS_ORDER.map(k=>'<td class="n'+((b.status[k]||0)?'':' muted')+'">'+(b.status[k]||0)+'</td>').join('')+ +'</tr>').join('');+ out.push(card('wide','Per person',+ 'Every owner in the plan, unassigned included, sorted by hours. Milestones are dates rather than work, so they carry no hours and are counted separately as +nM, but they do carry a status and are counted in the four columns on the right.',+ m.people.length?('<table class="an-table people"><thead><tr><th>Owner</th><th>Share</th><th class="n">Hours</th><th class="n">%</th><th class="n">Items</th>'+ +STATUS_ORDER.map(k=>'<th class="n">'+esc(statusTextFor(k))+'</th>').join('')+ +'</tr></thead><tbody>'+ +ph+'<tr class="total"><td>Total</td><td></td><td class="n">'+esc(hrs(m.totalHours))+'</td><td class="n">100%</td><td class="n">'+m.tasks.length+(m.ms.length?' +'+m.ms.length+'M':'')+'</td>'+ +STATUS_ORDER.map(k=>'<td class="n">'+stTot[k]+'</td>').join('')+ +'</tr></tbody></table>'):'<div class="an-empty">Nothing in this plan carries dates yet.</div>')); // 3. status const stTotal=Object.values(m.status).reduce((a,b)=>a+b,0);@@ -7799,16 +7968,7 @@ function renderAnalytics(){ out.push(card('','Status','Every task and milestone in the plan, by the status someone set on it. A row nobody has touched counts as not started.', '<table class="an-table"><thead><tr><th>Status</th><th>Share</th><th class="n">Items</th><th class="n">%</th></tr></thead><tbody>'+stRows+'</tbody></table>')); - // 4. status per person- const pph=m.people.map(b=>'<tr><td class="name'+(b.owner?'':' muted')+'">'+esc(b.owner||'unassigned')+'</td>'- +STATUS_ORDER.map(k=>'<td class="n'+((b.status[k]||0)?'':' muted')+'">'+(b.status[k]||0)+'</td>').join('')- +'</tr>').join('');- out.push(card('wide','Status per person','The same four counts, one row per owner.',- m.people.length?('<table class="an-table tight"><thead><tr><th>Owner</th>'- +STATUS_ORDER.map(k=>'<th class="n">'+esc(statusTextFor(k))+'</th>').join('')- +'</tr></thead><tbody>'+pph+'</tbody></table>'):'<div class="an-empty">Nobody owns anything yet.</div>'));-- // 5. phases+ // 4. phases const lr=m.lanes.map(l=>'<tr><td class="name"><span class="an-chip" style="background:'+esc(l.color)+'"></span>'+esc(l.name)+'</td>' +'<td class="n">'+l.items+'</td><td class="n">'+esc(hrs(l.hours))+'</td>' +'<td class="n muted">'+(l.from?esc(fmt(l.from))+' to '+esc(fmt(l.to)):'no dates')+'</td></tr>').join('');@@ -7817,7 +7977,7 @@ function renderAnalytics(){ +lr+'<tr class="total"><td>Total</td><td class="n">'+m.rows.length+'</td><td class="n">'+esc(hrs(m.totalHours))+'</td>' +'<td class="n">'+(m.first?esc(fmt(m.first))+' to '+esc(fmt(m.last)):'')+'</td></tr></tbody></table>'):'<div class="an-empty">This plan has no sections.</div>')); - // 6. milestones+ // 5. milestones const mr=m.ms.map(r=>{const d=daysFromToday(r.date); return '<tr><td class="name">'+esc(r.name)+'</td><td class="n">'+esc(fmt(r.date))+'</td>' +'<td class="n'+(d!==null&&d<0?' muted':'')+'">'+esc(relDays(d))+'</td>'@@ -7826,7 +7986,7 @@ function renderAnalytics(){ m.ms.length?('<table class="an-table"><thead><tr><th>Milestone</th><th class="n">Date</th><th class="n">From today</th><th class="n">Owner</th></tr></thead><tbody>'+mr+'</tbody></table>') :'<div class="an-empty">This plan has no dated milestones.</div>')); - // 7. dependencies+ // 6. dependencies const dep=m.deps; out.push(card('','Dependencies','What waits on what. The graph crosses sections freely, which is why these counts are for the whole plan.', '<table class="an-table"><tbody>'@@ -7837,7 +7997,7 @@ function renderAnalytics(){ +'</tbody></table>' +(dep.cycle.length?'<div class="an-foot an-warn">'+esc(dep.cycle.map(n=>n.r.name||n.id).slice(0,6).join(', '))+' wait on each other, directly or in a ring. Auto-schedule leaves these exactly where they are.</div>':''))); - // 8. overdue+ // 7. overdue const or=m.overdue.map(r=>{const dl=deadlineOf(r),d=daysFromToday(dl); return '<tr><td class="name">'+(isMs(r)?'<span class="muted">◆ </span>':'')+esc(r.name)+'</td>' +'<td class="n">'+esc(ownerOf(r)||'unassigned')+'</td>'@@ -7902,7 +8062,7 @@ function syncPlanCtl(){ const a=$('btnAuto'), c=$('btnCrit'); if(a){ a.disabled=!has;- a.title=has?'Align all task dates so dependencies line up finish-to-start, skipping weekends'+ a.title=has?'Align all task dates so dependencies line up finish-to-start, skipping weekends and US federal holidays' :'Auto-schedule needs dependencies: nothing in this plan waits on anything yet'; } if(c){
package.json+8−4@@ -2,8 +2,8 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.44.0",- "description": "Interactive Gantt chart viewer for project timelines, with a table view and an analytics page over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. One click auto-schedules the whole plan so every dependency lines up finish-to-start on business days, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for hours per person, status, per-section totals, milestones, dependency stats and what is overdue. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",+ "version": "0.45.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.", "main": "server.js", "bin": { "adom-gantt": "server.js"@@ -55,15 +55,19 @@ }, { "label": "Auto-schedule",- "prompt": "auto-schedule the plan so every dependency lines up and nothing lands on a weekend"+ "prompt": "auto-schedule the plan so every dependency lines up and nothing lands on a weekend or a federal holiday" }, { "label": "Critical path", "prompt": "show me the critical path through this plan" }, {- "label": "Hours per person",+ "label": "Per person", "prompt": "open the analytics page and tell me who is carrying the most hours"+ },+ {+ "label": "Hours left",+ "prompt": "how many working hours are left between now and the end of this project" } ], "scripts": {
page.json+9−5@@ -2,8 +2,8 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.44.0",- "description": "Interactive Gantt chart viewer for project timelines, with a table view and an analytics page over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. One click auto-schedules the whole plan so every dependency lines up finish-to-start on business days, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for hours per person, status, per-section totals, milestones, dependency stats and what is overdue. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",+ "version": "0.45.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.", "main": "server.js", "bin": { "adom-gantt": "server.js"@@ -55,15 +55,19 @@ }, { "label": "Auto-schedule",- "prompt": "auto-schedule the plan so every dependency lines up and nothing lands on a weekend"+ "prompt": "auto-schedule the plan so every dependency lines up and nothing lands on a weekend or a federal holiday" }, { "label": "Critical path", "prompt": "show me the critical path through this plan" }, {- "label": "Hours per person",+ "label": "Per person", "prompt": "open the analytics page and tell me who is carrying the most hours"+ },+ {+ "label": "Hours left",+ "prompt": "how many working hours are left between now and the end of this project" } ], "scripts": {@@ -81,4 +85,4 @@ "name": "Drew Owens", "email": "[email protected]" }-}+}