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
Docs: the what-lives-where diagram on the main page, updated for 0.77.1 (one Owner filter for both lenses, field-by-field saves, catch-ups, the order kept grouped by section), with its HTML source and a render script; docs/RELEASING.md
7 files changed
+176−3
README.md+4@@ -417,6 +417,10 @@ with the edits if you choose to discard them. </tr> </table> +++*Your view stays in your browser; the plan is on the server and the same for everyone. [What lives where](client-vs-server/README.md) sorts every setting.*+ ## Keyboard shortcuts | Key | Action |
client-vs-server/README.md+4−2@@ -40,7 +40,7 @@ tab of the same plan in the same browser shares them; another computer starts fr | The lens: Gantt, Table, Analytics, Setup | Analytics and the owner cards on Setup are hidden for teammates | | Days, weeks or months; text size; light or dark; milestone style; Today line | | | Critical path highlight | Only the highlight. Auto-schedule, which moves dates, is a plan change |-| Name column width; table column widths, sort and owner pick | |+| Name column width; table column widths and sort | | | Which legend chips are folded away | The "18 tags" and "50 sections" chips | A browser that has never opened the plan starts from the plan's own defaults: the@@ -68,7 +68,9 @@ Saved by the adom-gantt server and shared with everyone the plan is shared with. Edits save about half a second after you stop, as the fields you changed and nothing else (0.77.0), the way a Google Doc does. Two people editing different fields of one row both keep their change; the same field at the same moment goes to the later one, in every tab,-and the person whose value was replaced is told.+and the person whose value was replaced is told. The server keeps the row order grouped by+section, the way every screen shows it, and a tab that falls behind or finds it disagrees+with the server fetches the whole plan and puts itself right (0.77.1). ## Live, never saved
client-vs-server/where-it-lives.htmladded+119@@ -0,0 +1,119 @@+<!doctype html>+<!--+ The "what lives where" diagram (client-vs-server/where-it-lives.png, shown on the main+ README and on this folder's sub-page). This file is its source: edit it when what lives+ where changes, then re-render the PNG with the test Chrome, at 1400 px wide and twice the+ pixels (see docs/RELEASING.md):++ node test/render-diagram.mjs++ Brand fonts: Familjen Grotesk (titles), Satoshi (text), JetBrains Mono (labels). They are+ used from the system when installed, and the first two from Google Fonts otherwise.+-->+<html lang="en">+<head>+<meta charset="utf-8">+<meta name="viewport" content="width=1400">+<title>What lives where</title>+<link rel="preconnect" href="https://fonts.googleapis.com">+<link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">+<style>+ :root {+ --bg: #0d1117; --card: #161b22; --line: #262c36; --border: #30363d;+ --text: #e6edf3; --text2: #9aa4b2; --muted: #6e7681;+ --purple: #9b7bff; --purple-line: #7c5cff; --teal: #1fc7b6; --amber: #e3b341;+ }+ * { box-sizing: border-box; }+ html, body { margin: 0; background: var(--bg); }+ body { width: 1400px; font-family: 'Satoshi', Arial, sans-serif; color: var(--text); }+ .frame { padding: 44px 44px 40px; }+ h1 { font-family: 'Familjen Grotesk', sans-serif; font-weight: 700; font-size: 28px; margin: 0 0 6px; letter-spacing: -0.2px; }+ .lede { font-size: 16.5px; color: var(--text2); margin: 0 0 30px; }+ .lede b { color: var(--text); }+ .cols { display: grid; grid-template-columns: 300px 372px 1fr 440px; gap: 0 18px; align-items: stretch; }+ .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 22px 22px 20px; }+ .card.browser { border-color: var(--purple-line); }+ .card.server { border-color: var(--teal); box-shadow: 0 0 0 1px rgba(31,199,182,.12), 0 0 28px rgba(31,199,182,.10); }+ .kicker { font-family: 'JetBrains Mono', monospace; font-size: 12.5px; letter-spacing: 2.6px; color: var(--muted); text-transform: uppercase; margin-bottom: 10px; }+ .browser .kicker { color: var(--purple); }+ .server .kicker { color: var(--teal); }+ h2 { font-family: 'Familjen Grotesk', sans-serif; font-weight: 700; font-size: 21px; margin: 0 0 6px; }+ .sub { font-size: 14px; color: var(--text2); margin: 0 0 14px; line-height: 1.4; }+ ul { list-style: none; margin: 0; padding: 0; }+ li { position: relative; padding: 9px 0 9px 22px; border-top: 1px solid var(--line); font-size: 15px; line-height: 1.38; }+ li::before { content: ''; position: absolute; left: 3px; top: 15px; width: 7px; height: 7px; border-radius: 2px; background: #5b6370; }+ .browser li::before { background: var(--purple); }+ .server li::before { background: var(--teal); }+ li b { font-weight: 700; }+ .key { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--amber); border: 1px solid rgba(227,179,65,.55); border-radius: 4px; padding: 1px 5px; margin-left: 6px; }+ .foot { font-size: 12.5px; color: var(--text2); margin: 14px 0 0; line-height: 1.45; }+ .arrows { display: flex; flex-direction: column; justify-content: center; gap: 86px; text-align: center; color: var(--teal); font-size: 13px; line-height: 1.38; }+ .arrows > div { max-width: 150px; margin: 0 auto; }+ .arrow { display: block; margin: 6px auto; width: 74px; height: 12px; }+ .live { margin-top: 18px; border: 1px dashed rgba(31,199,182,.7); border-radius: 12px; padding: 16px 20px; display: flex; align-items: center; gap: 18px; }+ .live .kicker { color: var(--teal); margin: 0; white-space: nowrap; }+ .live p { margin: 0; font-size: 15px; line-height: 1.45; }+ .chip { display: inline-flex; align-items: center; justify-content: center; width: 21px; height: 21px; border-radius: 50%; font-family: 'Familjen Grotesk', sans-serif; font-size: 9.5px; font-weight: 700; color: #0d1117; vertical-align: -5px; margin: 0 1px; }+ .so { margin-top: 16px; background: #1b1830; border-left: 3px solid var(--purple-line); border-radius: 0 10px 10px 0; padding: 13px 16px; font-size: 15px; line-height: 1.45; }+</style>+</head>+<body>+<div class="frame" id="diagram">+ <h1>Where each thing lives in Adom Gantt</h1>+ <p class="lede">Two kinds of state: how <b>you</b> are looking at the plan stays in your browser; the <b>plan itself</b> lives on the server and is the same for everyone.</p>+ <div class="cols">+ <div class="card tab">+ <div class="kicker">Only this tab</div>+ <h2>Gone on reload</h2>+ <p class="sub">Nobody else sees it. Not even your other tabs.</p>+ <ul>+ <li>The section, Milestone and Dependencies chips in the legend (hide or dim)</li>+ <li>What you have selected, the open editor, where you have scrolled</li>+ <li>Undo history, your own changes only<span class="key">Ctrl+Z</span></li>+ <li>"See what the team sees" preview</li>+ </ul>+ </div>+ <div class="card browser">+ <div class="kicker">This browser</div>+ <h2>Remembered, never shared</h2>+ <p class="sub">Your view only. Kept for this plan in this browser, across reloads.</p>+ <ul>+ <li><b>Collapse all</b> and each section's fold</li>+ <li><b>Owner</b> (one filter for the chart and the table), Current, Overdue and tag filters</li>+ <li>Lens: Gantt, Table, Analytics, Setup</li>+ <li>Days / weeks / months, text size, light or dark, milestone style, Today line</li>+ <li>Critical path highlight</li>+ <li>Name column width; table columns and sort</li>+ <li>Which legend chips are folded away</li>+ </ul>+ <p class="foot">Every tab of this plan in this browser shares these. Another computer or browser starts from the plan's defaults.</p>+ </div>+ <div class="arrows">+ <div>your changes go out field by field, in about a second+ <svg class="arrow" viewBox="0 0 74 12"><path d="M2 6h62" stroke="currentColor" stroke-width="2" fill="none"/><path d="M62 1l10 5-10 5z" fill="currentColor"/></svg></div>+ <div><svg class="arrow" viewBox="0 0 74 12"><path d="M72 6H10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 1L2 6l10 5z" fill="currentColor"/></svg>+ everyone else's arrive live; a tab that falls behind fetches the whole plan</div>+ </div>+ <div class="card server">+ <div class="kicker">The plan's server</div>+ <h2>The plan: same for everyone</h2>+ <p class="sub">Saved by the adom-gantt server, on every teammate's screen within a second.</p>+ <ul>+ <li><b>Every task and milestone:</b> name, dates, owner, status, notes, tags, effort, provenance link, dependencies</li>+ <li><b>Sections:</b> names, colours, leads, order; the row order, kept grouped by section; tag colours</li>+ <li>Anything that moves dates: dragging a bar, editing a row, <b>Auto-schedule</b> (owner only)</li>+ <li><b>Project dates and the working calendar</b> on Setup (owner only)</li>+ <li>Adding, deleting, pasting and importing rows</li>+ <li>The changelog (who changed what), daily backups</li>+ <li>Sharing and the Google Chat digest (owner only)</li>+ </ul>+ </div>+ </div>+ <div class="live">+ <div class="kicker">Live, never saved</div>+ <p>Who has the plan open <span class="chip" style="background:#1fc7b6">DO</span><span class="chip" style="background:#e3b341">AB</span>, and which rows each person has selected, the coloured rows with initials. The server passes it on and forgets it when they close the tab.</p>+ </div>+ <div class="so"><b>So:</b> when you press Collapse all, filter to Unassigned, or switch to the table, only your screen changes. When you drag a bar, change an owner or press Auto-schedule, everyone's does.</div>+</div>+</body>+</html>
client-vs-server/where-it-lives.png⋯ 1 unchanged line ⋯
docs/BUILD-NOTES.md+1−1@@ -1,7 +1,7 @@ # adom-gantt build notes Decisions taken while building, and the places where the plan and the code-disagreed. Newest first.+disagreed. Newest first. The release checklist is [RELEASING.md](RELEASING.md). ## Build notes, 2026-11-03 (0.77.1, sections and the row order, after an afternoon on the team plan)
docs/RELEASING.mdadded+21@@ -0,0 +1,21 @@+# Releasing adom-gantt++The list for every version. What changed and why goes in [BUILD-NOTES.md](BUILD-NOTES.md).++1. **Version.** Bump `version` in `package.json` and `page.json`.+2. **Gates, one after another.** `node test/server_test.mjs`, `node test/collab_test.mjs`,+ then `node test/ui_test.mjs`. Never two at once: they share test ports. When several+ people or agents run suites on one machine, wrap each run in the same `flock` lock file.+3. **What lives where.** If the release changes what stays in a tab, in the browser or on+ the server (a filter, a setting, how saving or catching up works), update+ `client-vs-server/where-it-lives.html` and the tables in `client-vs-server/README.md`,+ re-render the PNG with `node test/render-diagram.mjs`, and look at the PNG. The main+ README shows it under "Your view and the shared plan: what lives where"; check it still+ does.+4. **Build notes.** A new entry at the top of `docs/BUILD-NOTES.md`: what changed, why,+ and the test counts.+5. **Publish.** `adom-wiki pkg info adom/adom-gantt` first, in case another session+ published; then `adom-wiki pkg publish --org adom -y`, and `adom-wiki repo push` for+ anything the package leaves out. Check a fresh clone and the live page.+6. **Deploy.** Only with the plan owner's go. A changed `server.js` needs a restart; a+ changed page needs only the files. Either way, everybody with the plan open reloads once.
test/render-diagram.mjsadded+27@@ -0,0 +1,27 @@+// Renders client-vs-server/where-it-lives.html to where-it-lives.png: the "what lives where"+// diagram on the main README and the client-vs-server sub-page. 1400 px wide at twice the+// pixels, in the suite's own Chrome. Run it whenever the HTML changes (docs/RELEASING.md).+// node test/render-diagram.mjs+import puppeteer from './node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js';+import { readdirSync } from 'node:fs';+import { join, dirname } from 'node:path';+import { fileURLToPath, pathToFileURL } from 'node:url';+const HERE = dirname(fileURLToPath(import.meta.url));+const CHROME = process.env.CHROME_PATH || (() => {+ const dir = join(process.env.HOME, '.cache/puppeteer/chrome');+ const dirs = readdirSync(dir).filter(d => d.startsWith('linux-')).sort((a, b) => a.split('-')[1].localeCompare(b.split('-')[1], undefined, { numeric: true }));+ return join(dir, dirs.at(-1), 'chrome-linux64', 'chrome');+})();+const src = join(HERE, '..', 'client-vs-server', 'where-it-lives.html');+const out = join(HERE, '..', 'client-vs-server', 'where-it-lives.png');+const browser = await puppeteer.launch({ executablePath: CHROME, headless: 'new', args: ['--no-sandbox', '--disable-dev-shm-usage', '--hide-scrollbars'] });+try {+ const page = await browser.newPage();+ await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 2 });+ await page.goto(pathToFileURL(src).href, { waitUntil: 'networkidle0', timeout: 30000 }).catch(() => {});+ await page.evaluate(() => document.fonts.ready);+ const el = await page.$('#diagram');+ await el.screenshot({ path: out });+ const box = await el.boundingBox();+ console.log(`OK: wrote ${out} (${Math.round(box.width * 2)}x${Math.round(box.height * 2)})`);+} finally { await browser.close(); }