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.8.4: /export/svg 500'd on a hand-corrupted data file while every other path coped
generateSVG builds its own date range and its own toX, so it went through
neither the import guard nor the viewer's drop-and-warn: a file hand-edited to
carry a dateless row returned 500 'Cannot read properties of undefined (reading
split)' while the viewer, /export/json, /export/csv and /export/html all handled
the same file. It now filters with the same rowDatesOk the importers use, and
names what it left out in an XML comment rather than shipping a quietly shorter
chart.
Two more in the same function and the same defect class: a phase with no usable
start or end threw from the phase loop for the same reason, and a file where
nothing carries a date produced a negative width. Both guarded; the second draws
a single empty month.
Output is byte-identical to 0.8.3 on a clean file.
4 files changed
+55−13
docs/BUILD-NOTES.md+18@@ -3,6 +3,24 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-07 (0.8.4)++One residual from the 0.8.3 dateless-row work. The viewer, `/export/json`,+`/export/csv` and `/export/html` all coped with a hand-corrupted data file, but+`/export/svg` still returned a 500 (`Cannot read properties of undefined (reading+'split')`), because `generateSVG` builds its own date range and its own `toX` and+so never went through either of the guards. It now filters with the same+`rowDatesOk` the importers use, and names what it left out in an XML comment at the+top of the file rather than shipping a quietly shorter chart.++Two things fixed alongside it in the same function, same defect class: a phase with+no usable start or end threw from the phase loop for the same reason, and a file+where nothing at all carries a date produced a negative width. Both are guarded;+the second falls back to drawing a single empty month.++Verified byte-identical output to 0.8.3 on a clean file, so nothing about the+normal path moved.+ ## Build notes, 2026-08-07 (0.8.3, final adversary round) **The server's milestone ladder was missing the key rung the client had.** The v3
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-gantt", "title": "Adom Gantt", "type": "app",- "version": "0.8.3",+ "version": "0.8.4", "description": "Interactive Gantt chart viewer for project timelines, with a table view 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. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place. 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.8.3",+ "version": "0.8.4", "description": "Interactive Gantt chart viewer for project timelines, with a table view 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. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place. 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": {
server.js+35−11@@ -121,18 +121,19 @@ function appendLog(events, actor) { // against, and nothing ever says it was invented. A 400 that names the offending // rows is fixable in a minute and cannot quietly become someone's deadline. const DATE_RE = /^\d{4}-\d{1,2}(-\d{1,2})?$/;+// The same rule the viewer applies in gantt.html: a task needs a start and an end,+// a milestone needs a date, a group needs neither.+function rowDatesOk(r) {+ if (!r || r.type === 'group') return true;+ if (r.type === 'milestone') return DATE_RE.test(String(r.date || ''));+ return DATE_RE.test(String(r.start || '')) && DATE_RE.test(String(r.end || ''));+} function assertDates(items) { const bad = []; (items || []).forEach((r, i) => {- if (!r || r.type === 'group') return;- const who = r.id || r.name || r.label || `row ${i + 1}`;- if (r.type === 'milestone') {- if (!DATE_RE.test(String(r.date || ''))) bad.push(`${who} (milestone needs a date)`);- return;- }- if (!DATE_RE.test(String(r.start || '')) || !DATE_RE.test(String(r.end || ''))) {- bad.push(`${who} (task needs a start and an end)`);- }+ if (rowDatesOk(r)) return;+ const who = (r && (r.id || r.name || r.label)) || `row ${i + 1}`;+ bad.push(`${who} (${r && r.type === 'milestone' ? 'milestone needs a date' : 'task needs a start and an end'})`); }); if (bad.length) { const shown = bad.slice(0, 10).join('; ');@@ -740,7 +741,16 @@ function importJiraCSV(body) { function generateSVG() { const data = mergedData();- const { project, phases, items } = data;+ const { project, phases } = data;++ // Imports reject undateable rows, but a hand-edited data file never goes through+ // an importer, and this path used to 500 on one while the viewer and the other+ // three exports all coped. Drop what cannot be drawn, same rule as the viewer,+ // and say so in a comment rather than silently shipping a shorter chart.+ const allItems = data.items || [];+ const items = allItems.filter(rowDatesOk);+ const dropped = allItems.filter(r => !rowDatesOk(r))+ .map((r, i) => (r && (r.id || r.name || r.label)) || `row ${i + 1}`); const COL = 66; const ROW_H = 34;@@ -758,12 +768,21 @@ function generateSVG() { if (r.date && r.date > maxDate) maxDate = r.date; }); phases.forEach(p => {+ // A phase with no usable start/end is the same class of hand-edit as a+ // dateless row, and split() on it threw from the same function.+ if (!p || !DATE_RE.test(String(p.start || '')) || !DATE_RE.test(String(p.end || ''))) return; const ps = p.start + '-01'; const [ey, em] = p.end.split('-').map(Number); const pe = `${ey}-${String(em).padStart(2, '0')}-28`; if (ps < minDate) minDate = ps; if (pe > maxDate) maxDate = pe; });+ // Nothing in the file carried a date. Draw an empty month rather than a chart+ // with a negative width.+ if (minDate > maxDate) {+ const now = new Date();+ minDate = maxDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;+ } const [minY, minM] = minDate.split('-').map(Number); const [maxY, maxM] = maxDate.split('-').map(Number);@@ -785,7 +804,12 @@ function generateSVG() { const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); - let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}">+ // "--" cannot appear inside an XML comment, and these names come from the file.+ const dropNote = dropped.length+ ? `\n<!-- ${dropped.length} row(s) left out: no usable date. ${esc(dropped.slice(0, 12).join(', ')).replace(/-{2,}/g, '-')}${dropped.length > 12 ? `, and ${dropped.length - 12} more` : ''} -->`+ : '';++ let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}">${dropNote} <defs> <style> .bg { fill: #0d1117; }