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
Publish 0.66.2
6 files changed
+20−6
SKILL.md+1−1@@ -231,7 +231,7 @@ rather than a category. ## Features -- **New** button (or Shift+N): create a task or milestone. After a successful create, the next New panel prefills EVERY field from the one you just made (type, phase, owner, status, start, deadline or milestone date, notes, provenance label, and since 0.60.0 its tags). Only the name starts empty, so a run of tasks for the same person in the same lane in the same week is names and nothing else. A provenance left at its default is not remembered as a value, so it keeps following the signed-in identity. It is in-memory for the page session only: a refresh clears it, and it is never written to localStorage, the preferences or the sidecar, so it is never carried to anyone else. Only a create that produced a row updates it: Cancel, Escape and a validation-rejected create all leave it untouched. Created rows live in the sidecar as `added: [...]`, keyed in a `#new-` namespace that cannot collide with the data file's `#row<n>`, and are merged into every export+- **New** button (or Shift+N): create a task or milestone. After a successful create, the next New panel prefills every field from the one you just made (type, phase, owner, status, start, deadline or milestone date, provenance label, and since 0.60.0 its tags). The name and, since 0.66.2, the notes start empty, because a note is about the one row it was written for, so a run of tasks for the same person in the same lane in the same week is names and nothing else. A provenance left at its default is not remembered as a value, so it keeps following the signed-in identity. It is in-memory for the page session only: a refresh clears it, and it is never written to localStorage, the preferences or the sidecar, so it is never carried to anyone else. Only a create that produced a row updates it: Cancel, Escape and a validation-rejected create all leave it untouched. Created rows live in the sidecar as `added: [...]`, keyed in a `#new-` namespace that cannot collide with the data file's `#row<n>`, and are merged into every export - **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. **It survives a refresh in the live app AND in an exported HTML snapshot** (fixed in 0.57.0). Text size is a READER preference, so it is the one pref a snapshot reads back from and writes to the reader's storage; everything else in a snapshot still opens at the author's view, and a snapshot now writes only the size, so it can no longer overwrite the grain, lens or collapse set a reader has stored on the same origin. Open tabs of the app follow a size change made in another tab live. **Every pref is saved merge-on-write**: a save writes only the keys this tab changed since it last read or wrote them, into a fresh read of storage, so a second tab saving its theme can no longer reopen the lanes you collapsed in the first, or reset your text size **On a page the host sandboxes (0.58.0)**, such as a snapshot uploaded to wiki.adom.inc and served under `Content-Security-Policy: sandbox` with no allow-same-origin, the page has an opaque origin and every reload starts with empty storage. A snapshot therefore also keeps the size in the URL fragment (`#fs=1.2`, written with replaceState), which a reload, a bookmark and a shared link all keep. Storage still wins where it works, so a reader's own size is not replaced by a link carrying somebody else's; the fragment answers only where storage has nothing. - **Project Duration and Time Remaining** in the header, stacked, beside the lens switch (0.57.0, replacing the old "25 weeks" axis-cell count, which changed with the grain and counted the axis padding). Both come from the plan's own dates and never from the grain: Duration runs from the earliest task start or milestone date to the latest task end or milestone date, both days counted, and Remaining runs from today to that last date, today counted. **Cancelled rows are left out**, as Auto-schedule, the critical path and the hours totals leave them out, and the tooltip says how many were; if every dated row is cancelled both read `none` and the tooltip says why. (The start button still counts cancelled rows, because it moves the whole plan. The Analytics "calendar span" card still includes them; it was not changed.) Format: days under two weeks, `16w 4d` style under half a year, then months and years to one decimal. The tooltips give the exact calendar and working-day counts and both dates. Remaining reads `ended` once the last date is past; before the first date it still counts from today, so it can be longer than Duration, and a small `not started` note sits on the readout itself to say why. Both read `none` for a plan with no dated rows. They update on every edit that can move a date (drag, editor, table edit, start shift, auto-schedule, create, delete, paste, undo, redo) and at midnight on a tab left open. The header FITS ITSELF to its content rather than to pixel breakpoints (`fitHeader`): it measures the row and, only while the content is wider than the window, sheds in order the version, the button words and the long labels (Duration, Remaining). If that still does not fit, it wraps onto a second row with the long labels back, and with the button words back too whenever they cost no extra row (they stay off on a phone, where they would). The readouts never shrink, so they are whole at every width; the suite sweeps 360 to 2000px with and without an appName at 100% and 150% text to hold that - **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
docs/BUILD-NOTES.md+9@@ -3,6 +3,15 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-10-14 (0.66.2, the New panel stops carrying the notes)++Drew: "do not persist note field between new task creation". The New panel's memory+(0.37.0) carried every field but the name into the next New, so a run of rows for one+person in one lane in one week is names and nothing else. The notes came along with the+rest, and a note is about the one row it was written for: the next row arrived saying+"Remove clamp slots?" about something else. The notes now start empty like the name;+the owner, lane, status, dates, effort, provenance and tags are still carried.+ ## Build notes, 2026-10-14 (0.66.1, a click on a name renames it in place again) Drew: "i should be able to edit a task name just by clicking on it, rather than edit task
gantt.html+6−3@@ -10063,7 +10063,10 @@ function openItemModal(r,carriedName,opts){ // same rendering paths as data-file content, which is where the escaping lives. // What the last successful Create left behind, so adding six tasks for the same // person, in the same lane, in the same week, is six names typed and nothing else.-// EVERY field is carried except the name. In-memory on purpose: a refresh clears+// Every field is carried except the name and, since 0.66.2, the notes: a note is about+// the one row it was written for (Drew: "do not persist note field between new task+// creation"), where the owner, the lane, the dates and the tags are usually shared by a+// run of rows. In-memory on purpose: a refresh clears // it, and it is never written to prefs, localStorage or the sidecar, so it is a // fact about this tab in this sitting and not about the plan or about anyone else // reading it. Cancel, Escape and a rejected create never touch it; only a create@@ -10095,7 +10098,7 @@ function openCreateEditor(){ const ownerVal=memo.owner||''; const startVal=memo.start||today; const endVal=memo.end||plus;- const descVal=memo.desc||'';+ const descVal=''; // never carried (0.66.2), see above // Who added it, by name, when there is a name to use. Still an ordinary editable // field: this is a default, not a stamp. A provenance the user actually TYPED is // carried; one they left alone is not stored at all, so the default keeps@@ -10203,7 +10206,7 @@ function openCreateEditor(){ // recorded as absent so the next panel recomputes the default from the // signed-in identity instead of freezing today's name into the session. lastCreated={type:isMs?'milestone':'task', phase, owner:owner||'', start, end,- status:st||'', desc:desc||'', tags:tags.slice(), effort:isMs?(lastCreated&&lastCreated.effort):eff,+ status:st||'', tags:tags.slice(), effort:isMs?(lastCreated&&lastCreated.effort):eff, prov:(prov===provDefault)?undefined:prov}; addedItems.push(r); insertRow(r);
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.66.1",+ "version": "0.66.2", "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.66.1",+ "version": "0.66.2", "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+2@@ -1713,6 +1713,8 @@ async function editorFlows(browser, S) { check('New saves type, section, owner, status, dates, description and provenance', !!t && t.type === 'task' && t.phase === 4 && t.owner === 'Quinn' && t.status === 'blocked' && t.start === '2026-11-02' && t.end === '2026-11-06' && t.description === 'new desc' && JSON.stringify(t.provenance || '').includes('typed-prov'), JSON.stringify(t)); await undo(page); await back('create with every field'); await page.click('#btnNew'); await settle(page, 150);+ check('the next New panel starts with empty notes (0.66.2) but still carries the owner', await page.$eval('#ncDesc', e => e.value) === '' && await page.$eval('#ncOwner', e => e.value) === 'Quinn',+ JSON.stringify([await page.$eval('#ncDesc', e => e.value), await page.$eval('#ncOwner', e => e.value)])); await page.select('#ncType', 'milestone'); await settle(page, 100); await page.type('#ncName', 'Fielded gate'); await setVal(page, '#ncEnd', '2026-11-20'); await page.click('#ncCreate'); await settle(page, 800);