master
Download ZIP Propose a change
Name Last commit message Last updated
client-vs-server Publish 0.80.0 · Drew Owens
docs Publish 0.80.0 · Drew Owens
examples Publish 0.3.1 · Drew Owens
screenshots 0.79.0: subtasks and quick add (folded until you open them, + on hover), one layout for every edit panel, the ClickUp i… · Drew Owens
test Publish 0.80.0 · Drew Owens
.adomignore 0.57.0: collapse/expand all sections, text size persists across refresh (live, two tabs, snapshot), decluttered header,… · Drew Owens
.gitignore Publish 0.78.0 · Drew Owens
favicon.svg Publish 0.1.0 · Drew Owens
gantt.html Publish 0.80.0 · Drew Owens
install.sh 0.80.0: requests. Giving a task to somebody else, or making one for them, asks them first: they accept or decline, the… · Drew Owens
package.json Publish 0.80.0 · Drew Owens
page.json Publish 0.80.0 · Drew Owens
README.md Publish 0.80.0 · Drew Owens
server.js Publish 0.80.0 · Drew Owens
SKILL.md Publish 0.80.0 · Drew Owens
uninstall.sh 0.80.0: requests. Giving a task to somebody else, or making one for them, asks them first: they accept or decline, the… · Drew Owens

adom-gantt

Interactive Gantt chart viewer for project timelines. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser.

Features

Auto-schedule the whole plan, by pushing later only. One press of Auto-schedule moves every task out until it starts no earlier than the work it waits on has finished, and so that nothing starts or ends on a Saturday, a Sunday or one of the plan's days off (the US federal holidays unless the plan works them, see Working calendar). The handoff is a MINIMUM rather than an answer. A task that starts BEFORE its latest dependency's end is pushed out to it, so the two bars meet on the chart with no column of white between them; a task that already starts on or after it is LEFT EXACTLY WHERE IT IS. That is what keeps a gap you meant: an order placed on the Monday and the parts that arrive on the Thursday keep their three days, because a lead time is real elapsed time and not a tidying-up job. Nothing is ever pulled EARLIER, so a plan cannot drift backwards over a press and a second press moves nothing at all. Pushes cascade, so a task moved out moves its own successors, measured against where they landed. 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 take the same rule with no duration. Cancelled work is out of the pass entirely: it never moves, and it constrains nobody, so a task whose only dependency is cancelled behaves like an anchor and the toast counts the excluded rows as their own number. 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, one per section. Critical lights up the LONGEST run of work inside every section at once: the bars, the dependency arrows along it and any milestone on it glow white and breathe, in a saturated indigo on paper. The dependency graph is read as a DAG, a task weighs its duration in WORKING days (weekends and the plan's days off excluded) and a milestone weighs nothing, and the critical path is the maximum-weight path through it. So it is the hardest run of work and not merely whatever chain happens to end last. A gap between two tasks adds no weight, because the length is the sum of the work along the path and not the calendar it is spread over, so the gaps Auto-schedule now preserves cannot promote or demote a chain. Cancelled work is not in the graph: it can never glow, it carries no weight, and a chain that ran through it is broken at that row, so the section falls back to whatever the longest run through the work that is still live turns out to be. Un-cancelling puts the row, its links and every path through it straight back, because nothing was deleted. Nothing is rescheduled: it reads the dates the plan has right now, independently of Auto-schedule.

A plain on/off toggle. Each lane is answered on its OWN subgraph, so a dependency that crosses sections is not an edge and never lights up, and a section with no dependencies among its own rows lights its longest single row. Ties are broken deterministically: more working days, then a greater calendar span, then more rows, then whichever starts earlier in the plan. Everything else stays exactly as it was, selection and dependency mode included, the glow recomputes as you edit, and the state is remembered across reloads.

Analytics. A third lens, next to Gantt and Table. Total effort for the plan in hours, summed from each task's Effort estimate (since 0.63.0; a task nobody has estimated adds nothing and is counted as not estimated, because a bar is the window the work happens in, not what it costs), 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 five status counts, the unassigned bucket included. Status counts overall, per-section totals with the range each section actually covers, per-tag totals when the plan has tags (since 0.60.0: items, hours and share, by each row's own tags plus its section's, with a "no tag" line), 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.

Setup. A fourth lens, since 0.58.0, for the plan's structure rather than its work. It is where the sections are managed: add one, rename it and set its aside, recolour it, move it, or remove it, each row showing how many rows the section holds and the dates they span. Since 0.61.0 a section moves by dragging its row (or Alt+Up / Alt+Down on a focused row), and its name and aside are edited by clicking them. Since 0.59.0 it is also where sections are tagged, one by one or from their name prefixes in one click, and where a tag is renamed or removed everywhere (since 0.60.0, on rows as well as sections). Since 0.61.0 it is where a tag is made before anything carries it (New tag) and where a tag gets a colour. It replaces the Sections button that used to sit in the header and open a modal. Every change is still one Ctrl+Z and one changelog line.

Four lenses over one plan. A Gantt / Table / Analytics / Setup 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 filter (the same control as the chart's, in the same place in the legend line), 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.

Add rows. The New button creates a task or a milestone: name, lane, owner, dates, status, notes and provenance. Created rows live in the sidecar, so the data file is never rewritten, and they appear in every export.

Quick add (0.79.0). Hover a section's header and a + appears beside its chevron: click it and a blank row opens at the end of that section with the cursor in it. Type a name and press Enter: the task is made and the row stays open for the next one, so adding a list of tasks is names and Enter. Esc, or Enter on an empty row, stops. The + on a task row does the same for its subtasks, indented under it. In the Table the + in a row's Section cell adds a task to that section, the blank row opening under that row whether or not the Table is sorted. There is no "+ Add task" line under every section: quick add is the + on hover. A quick-added task starts from what New remembers (owner, status, tags) and New's default dates (today, one week), then from the view's own filters, so under the Owner filter it is that person's and stays on screen. Everything else is edited afterwards. Each task is one Ctrl+Z and one changelog line, and quick add leaves Dependency mode first, as New does.

Subtasks (0.79.0). A task can hold subtasks, up to three levels under a top-level task (ClickUp's default). A subtask sits under its task in the chart's label column and in the Table, indented, and the task wears a chevron and a count. Subtasks start folded (ClickUp's default): a task shows its chevron and count, and its chevron opens them, for you only, remembered in your browser, while everybody else's stay as they had them; a folded task's bar shows its subtasks' span, earliest start to latest end, as a bracket under it. Collapse all and Expand all fold and open them too. To nest a row, drag it onto the middle of a task, or select it and press Alt+Right to put it under the row above it; Alt+Left takes it out to straight after its task, and dragging it to a gap with the pointer moved left does too (the drop line is indented to the level the row will land at). Moving a task moves its subtasks with it, into another section too. Subtasks are full tasks: their own dates, owner, status, effort, tags and dependencies, and Auto-schedule, Critical, the filters and Analytics treat them like any other task. A task's dates are its own, never forced to span its subtasks. In the Table a task's Effort shows its own hours and, after a dot, its hours with its subtasks'; Analytics counts every task's own hours once, so nothing is counted twice. Deleting a task deletes its subtasks, and one Ctrl+Z brings them all back. When a filter matches a subtask but not its task, the task stays on screen, dimmed, so the subtask is never shown out of context. Under a Table sort every row stands on its own, a subtask marked with an arrow.

Five statuses, in lifecycle order. Not started, in progress, complete, then the two ways off that road: blocked and cancelled. Every surface takes that order from one list, so the panels, the create panel, the status filter, the inline pill list, the analytics status card, the per-person card's five columns and the table's status-column sort all read the same way, and sorting by status ranks a finished row ahead of a stuck one. Plans written against the older "live" and "planned" still read correctly without being changed, and so do the American "canceled" and a tracker's "won't do".

Cancelled is work that will not happen, which is a different thing from work that is finished. The row stays on the plan, because deleting it would lose why it was ever there and what used to depend on it, but its HOURS leave every total: the project total, the owner's load, the section's effort and every percentage computed from them. Its own numbers are untouched, so the bar, the tooltip, the export and Ctrl+Z all still say what they said. It still counts as an item and it still has a column on the status cards, because it is a decision somebody made and worth seeing. It is never overdue. On the chart the bar is dimmed and the row is muted, well short of the fade a filtered-out row gets, and the pill carries a line through it.

Edit any row. Hover a row in the chart's label column and an Edit button appears at its right edge: one click opens the full editor for every field. Single-clicking the name still edits it in place.

Resizable table columns. Drag the divider between any two headers, or double-click it to fit the widest value. Widths persist.

Collapse a lane. The chevron at the right of each phase header hides that lane's rows and shows how many. Collapsing hides rows, not time: the axis does not move and Fit still fits the whole plan. This is your view only, it is not part of the plan. Collapse all at the start of the legend folds every lane to its header in one press, and reads Expand all once they are all folded; the table follows the chart.

Rename in place. Click a name in the chart's label column to edit it right there; double-click for the full editor with every field. Drag a task across a lane boundary and it changes lane, one undo for the whole move.

Drag to reorder. Click and hold a row for a moment to lift it, then drop it where it belongs. Dropping into another lane moves it there, position and phase in one step and one undo. Drag a lane header on the chart to move the whole lane. Dates read 17-Aug-2026 throughout; stored data stays ISO.

Edit the plan from the table. Names, phase, owner, dates, status, notes and provenance are all editable in place, and a row can be deleted with one click and an undo toast. Moving a row to another phase re-homes it under that lane's group header on the chart, not just in the data. Deletions are recorded explicitly in the sidecar, so restoring a saved plan can never confuse "removed on purpose" with "row lost".

Provenance. Every item can say where it came from: "provenance": {"label": "POR", "url": "https://..."}, or just "Drew added this". The table shows it as a chip, linked when the url is http or https and as plain text otherwise, so a hostile url can never become a link.

Owner is a first-class field. Put "owner": "Adonis" on a task or a milestone: one name, never a team. It drives the table's owner filter, it round-trips through CSV and JSON export, and changing it is recorded in the changelog like any other edit.

Visual timeline. Color-coded phase bars, diamond milestones, dependency arrows, and a live "today" marker. Date range and month count auto-computed from data.

Click-to-edit. Click any bar to open an inline editor with date pickers and dependency management. Drag bar edges to resize start/end dates. Drag bar body to slide dates while preserving duration.

Status is editable from the chart. Click a task's status pill in the label column to change it from a small on-brand dropdown (not started / in progress / complete / blocked / cancelled). It is the same edit as the table's status cell: one undo, logged to the changelog, reflected in both lenses.

Esc gets you out of a field. Every text box in the app hands the keyboard back when you press Escape: the table's filter box, the panel fields, the section editors, the type-in on the owner list, the date fields. The text you typed stays exactly where it is, so a filter you built is never wiped by a stray key, and the shortcuts are live again immediately rather than after a trip to the mouse. One Escape is one action: it never blurs a field AND closes something behind it in the same press.

Owner is editable from the chart too. Right of the status pill, every task row carries an owner pill, showing who has it, or NO OWNER in dim capitals when nobody does (the Table and a section with no owner on Setup say the same). Click it for a list of everyone already on the plan, plus a field for a name that is not on it yet; a new name is offered to every other row the moment it commits. Same path as the table's Owner cell, so it is one undo, one changelog event, and both lenses agree. The list scrolls when a plan has more people than it can show, by the wheel, the trackpad or the scrollbar; Escape closes it, so does a click anywhere outside it, and so does scrolling the plan out from under it.

Rename a row in place. Click the name text in the label column to edit it. Enter commits the new name, closes the field and leaves the row selected, so the rename hands straight over to the keyboard. Escape cancels.

Dependency arrows are coloured by where they come FROM. Each arrow takes the phase colour of the task at its tail, so a chain's origin is readable at a glance instead of every arrow being the same teal. A lane colour chosen to work as a wide bar can be invisible as a thin line, so the stroke is derived rather than copied: it is lifted or deepened away from the page, holding its hue, until it clears a 3:1 contrast ratio, which means a lane painted navy gets a lighter blue arrow that still reads as that lane. Picking an arrow in dependency mode, arming one for deletion and lighting the critical path all still override the colour while they are active. An arrow into or out of cancelled work is still drawn, dimmed by the same fraction the cancelled bar takes, so a link the schedule no longer walks recedes exactly as far as the work it points at.

One edit panel, one layout. Every panel that edits a task looks and works the same: the item editor on a task or a milestone, opened from the chart or from the Table, the New panel and the batch panel. A title that reads as a title, a grid of labelled fields, the buttons along the bottom. It is 400px wide at 100% text and grows with the app's text size like everything else (480px at 120%), and it is never wider than the window: on a phone, fields that sit side by side on a laptop stack one to a row. A long name, notes, bar label, owner or provenance wraps, and the field grows to show all of it, as you type and when the panel opens. A panel taller than the window scrolls its fields inside itself with the buttons staying put, and it never reaches into the strip along the bottom edge where the selection chip, the toasts and the save indicator live, so nothing ever covers Apply. Enter applies and Escape cancels in all of them.

The full item editor is the New panel. Editing a task or a milestone opens the same layout, in the same order, with the same labels, as creating one: type and section, name, owner and status, the dates, effort, notes, provenance and tags. Type is shown but not editable on an existing row, because a task has two dates and a bar and a milestone has one date and a diamond, and dropping one of a task's dates is not a conversion. The buttons are Apply, Cancel and Delete, with Delete kept away from the other two.

Every edit panel remembers where you put it. A panel opens beside what you clicked (New under the New button, the batch panel in the middle of the window) and is dragged by its title. Drag it once and every panel after it opens in that same spot on screen, whatever row it belongs to and however far the chart has been scrolled, until the page is reloaded. Nothing is saved to the plan; it is a property of the sitting, and a refresh restores the automatic placement beside the bar.

Select a row without opening anything. Every task and milestone row has a handle on its left edge, six dots that show when you point at the row: in the chart at the start of the name, in the Table at the start of the ID. A click on the handle selects that row and nothing else happens: no rename, no picker, no editor. Ctrl+click on a handle adds the row or takes it out, Shift+click selects the rows in between (only the ones on screen), and Escape clears. Then press Delete twice to delete the selection, or use Ctrl+C, Alt+Up and Alt+Down, or Copy IDs. Drag the handle up or down to move the row. A selected row keeps its handle showing, in the accent colour. Your selection is this tab's own and is never saved.

Multi-select. Ctrl+click to toggle individual rows, Shift+click for range select (Windows Explorer style), in either lens. Tasks and milestones both, since both have a permanent id to be named by; lane headers do not join. The batch editor applies relative date shifts across the selected tasks without flattening dates, and says how many milestones it is leaving alone: a milestone has one date and no duration, so shifting its end has no meaning. Its Delete removes the whole selection, milestones included, in one press and one Ctrl+Z: the same path the chart's armed Delete key uses, so it is one undo entry, one changelog batch, dependencies on the departed rows dropped and their ids retired either way.

Milestones are gates (0.78.0). A milestone can wait on tasks, a task can wait on a milestone, and one milestone can wait on another: "EVT sign-off" waits on the EVT build and test, and production tooling waits on the sign-off. Make the link the way you make any other, in Dependency mode (click the diamond, or in the lines style the cap on the rule, as the first click or the second) or in the item editor, whose dependency list a milestone now has and whose "waits on" list offers milestones, marked with a diamond. Arrows run into and out of the diamond's points, and in the lines style onto the dashed rule level with the task at the other end. When the work before a gate slips, Auto-schedule moves the gate to the day that work ends and pushes the work after it along, starting on the gate's date at the earliest, the same rule as a task after a task. Critical carries a chain through a gate at no cost and lights the gate with it. The cycle check, copy and paste, the Table's Deps cell, the changelog, live editing and every import and export all take milestone links. Every milestone has an id for this, made from its name the way a task's is: the server gives one to every milestone without one when it starts, so an existing plan needs nothing done to it.

Copy and paste rows, and the chain comes with them. Select one row or several, Ctrl+C, then Ctrl+V. With a row selected the copies land DIRECTLY BELOW it, in its lane, recoloured to match; with nothing selected they go to the end of the lane you are looking at (on the chart, the lane whose header is pinned under the axis; in the table, the lane of the topmost row fully clear of the sticky header). The part that makes it worth having is the dependencies: links AMONG the copied rows are remapped onto the copies, so duplicating a five-step build across another workcell gives you a five-step build and not five rows all hanging off the originals. Links pointing OUTSIDE the copied set are kept exactly as they are, still naming the original, because that is a real fact about the work. Nothing downstream of an original is touched: a task that waited on the row you copied still waits on that row and not on both. Names are KEPT, so you can rename after rather than undoing " (copy)" five times; the exception is a name that would collide inside the destination lane, which gets " 2", then " 3", counting past whatever is already there. Every copy gets a permanent id of its own, minted by the server; if that mint cannot be made the paste is refused whole and says so, rather than leaving half a chain. Milestones copy too. The pasted rows become the selection and are scrolled into view, ready for Alt+Arrow or a drag, and the whole paste is one Ctrl+Z and one line in the changelog.

Vertical reorder. Drag bars or labels up/down to reorder rows. Drop indicator shows insertion point. Multi-select reorder moves all selected items together.

Dependency arrows. Teal routes with rounded elbows and arrowheads show task relationships: out of the source bar's end sideways, down, and into the next bar's start sideways again, steering round any bar in the way rather than through it. Click any bar to see incoming ("needs") and outgoing ("blocks") dependencies. Add or remove dependencies inline.

Links to a row (0.73.0). Every task and milestone has a permanent ID like #00042. Each chart row has a link button at its right end, the Table's first column is the ID (click it), and the item editor has "Copy link #00042". Each copies #00042 Name: <plan address>#row=00042 for pasting into Chat (and a named link where the paste takes formatting). Opening the link selects that row, opens its section if it was collapsed, scrolls to it and marks it for a moment.

The owner's view and the team's (0.73.0). The plan's owner sees everything. Everyone else it is shared with gets the same plan and the same editing, but no Analytics lens and no Sharing or Google Chat settings on Setup (Sections and Tags stay). "See what the team sees" on the Sharing card previews that view until "Back to my view". Only the owner changes how often Chat is posted to.

Tags fold like sections (0.73.0). A plan with more than eight tags shows them behind one "N tags" chip in the legend; a chosen tag keeps its chip.

Provenance links stay intact. A row's provenance can be plain text ("Added by Drew") or a link to where it came from (a Chat thread, a document). A plain one is edited like any field. A linked one is shown as the link in the Table and in the item editor and its text cannot be changed there, so it always names what it opens (0.71.0).

Current and Overdue. Two chips in the legend, for reviewing a plan with the team. Current (0.70.0) shows only what has started (start date today or earlier) and is not complete or cancelled: the work under way, plus anything that should have started and has not. Future and finished rows are hidden, in the chart and the table. Overdue shows only what is past its deadline and not done. Turn both on for just the overdue rows. Each counts tasks and milestones separately, and neither changes the plan.

Legend filtering. Click any phase, "Milestone", "Dependencies" or "Today" in the legend bar to toggle visibility. Dimmed items fade to 15% opacity; hiding "Today" removes the marker line. The legend wraps onto as many lines as the plan needs, so a plan with twenty sections shows all twenty chips. When the sections carry tags (0.59.0), the legend leads with a chip per tag and the twenty section chips fold behind one "20 sections" chip; pick a tag and only the sections carrying it are shown, in the chart and the table, with their own chips back in the legend. Pick several to see any of them; Clear tags shows everything. It is your view only, and it survives a refresh. Since 0.64.0 the legend also leads with an Owner pick (every owner with their row count, plus Unassigned, shared with the table's owner filter) and an Overdue chip: rows whose end date or milestone date has passed without being marked complete or cancelled get a red edge and ring, and the chip shows only them. Overdue is worked out from the dates every time the page draws and at midnight; it is never written into the plan. Changing any of these filters, or Collapse all and Expand all, keeps your place: the row at the top of the chart stays where it is, or, if the filter hides it, the nearest row above it that still shows takes its spot. A row someone else adds or deletes above you does not move your view either (0.74.1). Scrolled all the way up, you stay at the top, and a section's chevron stays under the pointer when it folds (0.74.2).

Project dates. The first card on Setup holds the project's start and end (0.75.0). Project Duration runs from the start to the end and Time Remaining from today to the end, so the clock in the header is the project's commitment rather than wherever the rows happen to reach. The chart marks both dates with a dashed amber line and hatches the time outside them, so a bar that runs past the end is hatched where it does, the header readouts turn amber, and the card lists every row outside the dates with a link to it. Only the plan's owner can change them. With no dates set, everything counts from the rows as before. They replace the header's start button, which moved the whole plan for everyone.

Working calendar. The second card on Setup says which days this plan works (0.76.0). Weekends are always off, and by default so are all eleven US federal holidays; untick one your team works (Adom works Columbus Day and Veterans Day) and it becomes a normal working day for this plan. Add the weekdays the whole team is off, like a year-end shutdown, and they become days off. Auto-schedule, the critical path, every hours and workdays figure, and the day grain's shading all follow it, so a worked holiday is not shaded and an extra day off is. Only the plan's owner can change it; every change is one Ctrl+Z and one changelog line.

How long, and how long left. The header says Project Duration and Time Remaining, worked out from the plan's own first and last dates and today, never from the cell size, leaving cancelled work out, and updated on every edit. They stay whole at every window width: the header sheds button words, then wraps, before it lets them be cut. A printed chart names the plan in a title line of its own. Hover either for the exact dates and the calendar and working-day counts. The plan itself is named once, in the brand block at the far left; its title and description are on that block's tooltip.

New items remember the last one. After you create a task or milestone, the next New panel prefills every field from it: type, lane, owner, status, both dates, notes and the provenance label. Only the name starts empty, so adding several items for the same person in the same lane in the same week is just names. Leave provenance at its default and it stays a default, following whoever is signed in rather than freezing a name into the session. It lasts the page session only: a refresh clears it, and it is never saved to your preferences or the plan, so it is never handed to anyone else. Cancel, Escape and a create rejected for a missing name all leave the memory exactly as it was.

Undo. Ctrl+Z undoes date edits, dependency changes, drag operations, row reorders, and table edits to owner, status and notes (100-level stack).

Import. JSON (native format), CSV, MS Project XML, Jira CSV and ClickUp (see Importing from ClickUp). Drag-and-drop or file picker. Links to and from milestones come in with every one of them (0.78.0): an MS Project milestone arrives as a milestone with its predecessor links, a Jira issue of the type Milestone as a milestone, with Jira's Blocks links arriving as dependencies, and a ClickUp Milestone with the dependencies ClickUp's API gives it. A file with rows that carry no usable date is rejected whole, naming the rows, rather than having a date invented for it; a ClickUp task with no date at all comes in on the day of the import, tagged undated, instead. A row that cannot be dated is left out of the chart with a warning instead of blanking the plan.

Export. JSON, CSV, SVG, and self-contained HTML (no server needed to view the snapshot). The Export button serialises what is on your screen right now, not what is on disk, so it reflects unsaved edits and keeps working even when a save conflict has stopped autosave. CSV carries the phase by name, so an export re-imported keeps its phases, and a row's tags in a tags column joined with ; like its dependencies, and any cell opening with a formula character is apostrophe-guarded so a spreadsheet cannot execute it. Import strips the guard back off, and a note that runs over several lines comes back in whole.

Auto-save. Edits persist to a sidecar JSON file next to the data file, debounced while you work and flushed immediately if you close or hide the tab, so the changelog never claims an edit the sidecar did not get. Delete the sidecar to reset. Every row in it is keyed by its permanent uid, so renaming a row, reordering the plan, or inserting a row into the middle of the data file all leave every saved edit pointing exactly where it was. Sidecars written by older versions keyed rows by position; they are translated on the way in and keep working. The saved row order is applied all-or-nothing, so a key that no longer resolves can never drop a row.

Fills the window. Cells stretch to the width of the pane, and stop at each grain's minimum so a long plan scrolls horizontally rather than squashing into unreadability. Resizing reflows without a reload.

Light mode. Dark by default. The Light toggle switches to a print-friendly theme with deepened bar colors, and printing always uses it whatever the screen is set to.

Brand phase colors. A phase with no explicit color cycles the three Adom hues: teal, purple, blue. There is no background band behind a phase: the bar colour and the group header carry the section on their own, so colour on the chart means exactly one thing, which phase a bar belongs to. The hues are theme-aware, so the same plan is on-brand on screen and legible on paper.

Text size. A-minus and A-plus at the right end of the header step the whole app between 85% and 150% in seven steps, the chart, the table and the edit panels together. The percentage between them is the reset. Rows, bars and the axis grow with the type, so nothing is clipped, and the choice persists across a refresh, in the live app and in an exported HTML snapshot alike.

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, a differently coloured band on each of the plan's days off (the federal holidays it takes, with the name in a tooltip, and its extra days off), and a full calendar picker.

Editable milestones. Drag a milestone sideways to move its date, or click it to edit its name, date, description and (0.78.0) what it waits on and what waits on it. 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.

Bar notes. Put a short label on the bar body, the way durations are annotated on a printed plan. Double-click a bar to type one, or use the note field in the editor.

Resizable name column. Drag the divider between the names and the timeline, or double-click it to fit the longest name. The width persists.

Room to scroll. You can scroll past the last row, so the bottom of the plan can sit centred in the pane instead of pinned to the bottom edge.

Clean group labels. A trailing date range in a group label is dropped at render, since the axis already shows the dates and the text goes stale as soon as the plan moves.

Sticky axis and section headers. The date axis stays pinned at the top while you scroll the plan, and the header of whichever section you are in pins just beneath it, handing off to the next section's header at the boundary. Horizontal scroll still slides the axis with the bars so dates stay over their columns. A pinned header is the real row: click it to rename the section, use its chevron to collapse, drag it to reorder.

Authored default view. project.view in the data file pins how the chart opens: lens (mode), grain, milestone style, today marker, theme and name-column width. The HTML export bakes it in and ignores the viewer's saved preferences, so a shared snapshot always matches the image that invited the click. In the live app your own choices still win.

Changelog. Every edit is appended to <datafile>-changelog.jsonl with the item, the before and after dates, the day shift, and which gesture made it. An AI picking the project up later reads it and builds on your changes instead of overwriting them. The rev counter in the bottom-left corner is the handle: an agent notes it, then asks for only what changed since.

In action

Click a bar and the item editor opens beside it with every field of the row, among them the incoming ("needs") and outgoing ("blocks") dependencies, each removable, with a picker to add more.

The item editor beside its bar: every field of the row, the dependencies it waits on and the ones waiting on it, and the buttons

The layout scales to real plans. This is the bundled SMT production line example: 5 phases, 43 items, 8 milestones, striped bars for ongoing work:

SMT production line plan: 5 phases, 43 items, 8 milestones

Quick start

node server.js --data examples/sample-product-launch.json --port 8901

Open http://localhost:8901 in a browser, or point a Hydrogen webview tab at the proxy URL.

Data file format

Create a JSON file with three sections:

{
  "project": {
    "title": "My Project",
    "subtitle": "Short description",
    "start": "2026-01-05",
    "end": "2026-06-30",
    "calendar": { "workHolidays": ["Columbus Day", "Veterans Day"], "extraDaysOff": ["2026-12-24"] }
  },
  "phases": [
    { "name": "Phase 1 — Build", "lead": "the long pole (Adonis)", "start": "2026-01", "end": "2026-06" }
  ],
  "items": [
    { "type": "task", "id": "t1", "name": "Task name", "start": "2026-01-15", "end": "2026-03-01", "phase": 0, "owner": "Adonis", "status": "planned", "description": "Details.", "deps": ["other-id"] },
    { "type": "milestone", "id": "m1-goal-reached", "name": "M1: Goal reached", "date": "2026-06-01", "phase": 0, "description": "What this means.", "deps": ["t1"] }
  ]
}

project.start and project.end are the project dates and project.calendar the working calendar, all optional and all normally set on Setup. In the calendar, workHolidays names the federal holidays the plan works, spelled as the app lists them (New Year's Day, Martin Luther King, Jr. Day, Washington's Birthday, Memorial Day, Juneteenth National Independence Day, Independence Day, Labor Day, Columbus Day, Veterans Day, Thanksgiving Day, Christmas Day), and extraDaysOff lists YYYY-MM-DD days nobody works. Unknown names and malformed days are dropped. Without a calendar every federal holiday is a day off.

Tags are optional on a phase and on a task or milestone: "tags": ["Tech"], a short list of short strings. The server trims them, drops duplicates that differ only in case (the first spelling wins), caps each at 32 characters and the list at 12, turns a ; inside a tag into a space (; separates tags in the CSV), and writes no tags key at all when there are none, so a plan without tags round-trips unchanged. A tag change is saved in the sidecar and logged as tags (a row) or phase.tags (a section). Section tags travel in the JSON export and the HTML snapshot; a CSV is rows, so its tags column carries row tags only. That makes a CSV export and re-import lossy for tags: the section tags, and every tag a row only inherits from its section, do not come back. Use JSON (or the HTML snapshot) to keep them.

Since 0.59.0 section tags are edited on the Setup tab: a tags line under each section (type a tag, Enter or a comma to add it, the x on a chip to remove it, suggestions from the tags already in the plan) and a Tags card that lists every tag with how many sections carry it and renames or removes one everywhere. If sections are named with a prefix, like "Tech · Fab", one button tags each with its prefix; it adds tags and never renames. In the legend, a chip per tag leads the line and the section chips fold behind it: choose one or more tags to see only the sections carrying any of them, in the chart and the table. Every tag edit is one Ctrl+Z; the filter is your view only and survives a refresh.

Since 0.60.0 a task or a milestone can carry tags of its own. The full item editor and the New panel have a tags field (the same chip box; the New panel remembers the last row's tags like its other fields), the table has a Tags column you can click to edit and sort by, and the batch panel adds a tag to every selected row or takes one off all of them. Copy and paste carry a row's tags. A row's EFFECTIVE tags are its own plus its section's, and that is what the legend filters on: every row of a chosen section is shown, a row tagged "long-lead" in an untagged section is shown by "long-lead" (with its section header, and without its untagged neighbours), and a row that matches neither way is hidden. The Tags card on Setup counts rows as well as sections, and renaming or removing a tag there reaches the rows too. In the chart a row's own tags follow its owner as quiet #tag text, which gives way before the name ever does, and they are on its tooltip.

Since 0.61.0 tags are first-class. A plan can carry a tag registry, project.tags, a list of {"name": "Tech", "color": "#8c6bf7"}: it holds a tag's colour, and tags nothing carries yet. The Tags card on Setup has a New tag box (the tag is listed as "0 sections" and suggested in every tag box, but gets no legend chip until something carries it, since it would filter to nothing), click-to-rename on every tag's name, and a swatch that opens a palette: the three brand colours first (teal #00b8b1, purple #8c6bf7, blue #64abff), then coral #ff8a7a, amber #e8b64a, mint #5fd7a3, rose #ec7ab8, indigo #6f7cff and slate #8b9bb4, a hex box, and No colour. A coloured tag shows its colour wherever it is a chip or a pill (legend, section headers, Table pills, Setup, editors, Analytics, and the #tag text on chart rows): a light fill of the colour, with text and border walked to 3:1 contrast against the page on either theme. Bars keep their section's colour. Colours are lowercase #rrggbb, names follow the tag rules above, and a plan that never uses the registry carries no project.tags key. Registry changes are logged as tag.create, tag.rename, tag.color and tag.delete.

Subtasks (0.79.0): "parent": "00042" puts a task or a milestone under the task with that uid (a task's id works too, and is read as that task's uid). A parent must be a task, a row cannot sit under its own subtask, and subtasks go three levels deep under a top-level task; a parent the plan cannot take is dropped and the row stays a top-level row. A subtask is always in its top-level task's section, whatever its own phase says. Each row also carries pos, its place among its siblings: a fractional index key such as "a0", "a1", "a0V", generated by the app and compared as plain strings. Leave it out when writing a plan by hand or importing one: rows without it are placed in the order they are listed. The JSON and CSV exports carry parent (the CSV as a column after uid) and an import reads it back.

Omit color on a phase and it follows the Adom brand ramp, which adapts between dark and print. See SKILL.md for the full field reference and the ramp values.

Sections are phases

phases[] is the whole truth about the sections a plan has. Each one renders its own header row, in phases order, with its item count, including a section with nothing in it yet. There is no "group" item type any more: headers are derived, so renaming a header renames the phase and the change shows up in the legend, the table's phase column and every phase dropdown at once. Dragging a lane reorders phases[] itself, which is why the legend and the chart can no longer disagree.

The Setup tab is the editor (since 0.58.0; until then it was a modal behind a Sections button in the top bar): add a section, rename it or give it an aside by clicking the text, recolour it from a brand-safe palette (or a #rrggbb of your own), drag its row to reorder it (or Alt+Up / Alt+Down with the row focused), or remove it. Each section's row shows how many rows it holds and the first and last dates among them. An empty section goes on its own; one with rows in it makes you say where they land, so nothing is ever orphaned. Every change is one Ctrl+Z and one changelog line.

A lead is the short aside beside a section name ("the long pole (Adonis)"), shown as secondary text. Older data files that still carry type: "group" rows load normally: each is absorbed into its phase, contributing its aside as the lead.

Item ids that do not move

Every row carries a uid: a zero-padded number, assigned once, that nothing can change. It is written into the file for you, so you never type one.

{ "type": "task", "uid": "00042", "id": "t1", "name": "Task name", "...": "..." }

It exists because every other name a row has is conditional. A task id is a slug of the name the row had when it was made, so it reads as a lie after a rename, and since 0.78.0 a milestone's id is made the same way (the first start of a 0.78.0 server gives every milestone without one its id, after backing the data file and the sidecar up as .pre-msid.bak; the first start of 0.79.0 gives every row its place key the same way, keeping the sidecar as .pre-pos.bak). A group has no id at all and, like a milestone before 0.78.0, is tracked by its position in the file, which renumbers whenever the file is restructured. A uid is a number, it means nothing, and it survives a rename, a reorder, a move between lanes, a compact and a round trip through CSV.

  • Assigned once, in the order the plan reads, the first time a server on this version starts on your data file. It backs the file up first (.pre-uid.bak).
  • Handed out by the server, from project.nextUid, one at a time. A browser never invents one, so two people adding a row in the same second cannot collide.
  • Retired on delete. The number goes with the row and is never issued again, so a stale reference resolves to nothing rather than to somebody else's row.
  • Carried by every export, and read back by every import.

Select rows and press the Copy IDs button (or Ctrl+C) to get uid, a tab, and the name, one row per line: paste that into an AI chat, ask for a batch of edits, and every edit lands on the row you meant. Tasks and milestones can both be selected; lane headers cannot.

Row order, deletions and every saved edit are keyed by uid too, in both the data file and the sidecar. That is what makes it safe to insert a row into the middle of a plan: nothing below it is identified by its position any more. Sidecars written by older versions are translated on the way in and keep working.

Importing from ClickUp

A project kept in ClickUp comes across in one import. Like every import it replaces the whole plan (backed up first), and every row gets a fresh uid. Each row's provenance links back to its task in ClickUp.

Not yet checked against a real ClickUp project: compare a first import with ClickUp before relying on it.

Import, then ClickUp CSV takes either of ClickUp's CSV exports:

  • A List or Table view's export: in the view, Export view, CSV, all columns. Export a Folder's List view to bring every List in that Folder. Show subtasks as separate tasks first, or ClickUp leaves them out of the file. Any of its date formats reads.
  • The workspace export: Settings, Imports and Exports, Export. It holds the whole workspace, so POST /import/clickup-csv?list=<name or id> (or ?folder=, ?space=, comma separated) keeps only part of it.

Import, then ClickUp API JSON takes what ClickUp's API answers for a List's tasks, saved to a file. ClickUp's CSV exports do not include dependencies; this does.

# the List id is the number after /li/ in the List's link; a page holds 100 tasks
curl -s -H "Authorization: $CLICKUP_TOKEN" \
  "https://api.clickup.com/api/v2/list/<list id>/task?subtasks=true&include_closed=true&page=0" > page0.json
jq -s . page0.json page1.json > tasks.json      # more than 100 tasks: pages in one array

Or the server asks ClickUp itself: POST /import/clickup-api with {"list":"<id>"}, {"folder":"<id>"}, {"space":"<id>"} or {"url":"<a ClickUp List link>"}, and it follows the pages. The token comes from CLICKUP_TOKEN in the server's environment, or from a file only its owner can read: --clickup-token-file <path>, CLICKUP_TOKEN_FILE, or ~/.config/adom-gantt/clickup-token by default, with chmod 600. It goes to ClickUp in one header and nowhere else: never into the plan, the changelog or an answer. GET /import/clickup-api says whether a token is set up, never what it is.

How ClickUp maps onto the plan:

ClickUp adom-gantt
a List, and its Folder a section, "Folder · List". The Space goes in front when the file spans more than one
tasks rows, in ClickUp's order: the export's row order, or the API's orderindex
subtasks rows right after their parent, depth first, each naming its parent row's uid in parent (the subtasks model). A milestone is never a parent: its subtasks go under the nearest task above it, or to the top level. Three levels under a task is the deepest; anything deeper sits at the third
start and due date start and end. Only a due date: the bar starts on it. Only a start: it ends on it. A parent with neither spans its dated subtasks. A task with no date at all, subtask or milestone included, is a one-day bar on the day of the import, tagged undated, so a made-up date is easy to find and fix; the report and the changelog count them
assignees owner, the first one. The others are named in the notes ("Also assigned in ClickUp: ...")
status the five statuses. From the API, by ClickUp's status type (open, custom, done, closed). From a CSV, by name: to do, backlog and open are not started; in progress, review and testing in progress; complete, done and closed complete; blocked, on hold and waiting blocked; cancelled and won't do cancelled. A task with a done date is complete. Any other name reads as in progress, which is what ClickUp's middle statuses are, and the report marks it as a guess
time estimate effort, in hours
tags tags
description notes
the Milestone task type a milestone on its due date
dependencies (API) deps, to and from milestones too. A ClickUp row's id here is cu- and its ClickUp id, a milestone's as well as a task's

Dates are the days ClickUp shows. ClickUp keeps a date with no time of day at 04:00 in the zone of whoever set it, and the importer reads it in that zone, so nobody east of UTC finds every date a day early.

The report. Every ClickUp import answers with a report, and the page shows it once, after the reload, in the tab that imported: how many rows and sections came across, what changed on the way in (undated rows placed and tagged, subtasks kept under their parent, starts taken from due dates, parents spanning their subtasks, repeated rows merged), how each ClickUp status was read, and what did not come across. Not carried over, and counted: priority (by level), each custom field by name, checklists, comments, attachments, time tracked, sprint points, sprints, linked tasks, tasks that also sit in other Lists, team assignees, custom task types, and dependencies with nowhere to go. The changelog's import line lists what did not come across, so an agent reading the history later knows the plan never had it.

Sharing a plan with your team

Every page load and every change requires a signed-in Adom account. An unauthenticated visitor gets a sign-in page and nothing else: no plan data is sent before the session exists. Sign-in uses Authentication Intents ("Sign in with Adom"), the session lives in an HttpOnly cookie for 30 days, and it survives a server restart.

Your identity does three things once you are signed in. The header shows who you are signed in as, and clicking it gives you your details and a way to sign out. A new task defaults its provenance to "Added by ", still editable. And the changelog records what you did under your real name, so a shared plan's history reads "Drew Owens moved X" rather than "user moved X". The actor is taken from the session and never from the request body, so it cannot be spoofed.

Reading the plan from inside the container is still open. A GET of /data, /state, /changelog or /export/* over direct loopback (127.0.0.1, no proxy headers) needs no session, which is what keeps an AI agent's read-and-hand-off loop working. Anything that CHANGES the plan needs a signed-in person no matter where it came from. This is not a security boundary and does not pretend to be one: anything already running on loopback in the container can read the data file directly.

Run with --no-auth to turn all of this off for a single-user local session.

Who can open the plan (0.68.0). Each plan has a sharing setting on the Setup page: anyone signed in to Adom (the default), members of one Adom organization, or only the owner. The server enforces it on every request, including the live channel, so a change takes effect at once for everyone. Everyone it lets in has full control. The owner is named on the server (POST /access {"owner":"<username>"} with the agent token); only the owner can change the setting or set the Google Chat webhook. Before every import or compact, and once a day, the plan is backed up to <plan>-backups/ (last 30 kept).

Letting an AI agent write, without breaking the history

Reading is open over loopback, but writing needs a signed-in person, and an agent has no browser to sign in with. That left an agent asked to change a plan with one option: writing the data file and the changelog directly. The changelog's hash chain catches exactly that and reports it on /health as integrity.ok:false, so an agent edit was either indistinguishable from tampering or it never happened.

The agent token is the door for it instead. At startup the server writes 32 random bytes, hex, to a 0600 file beside the plan, and reuses that file forever after:

dc-demo.json                 the plan
dc-demo-state.json           the sidecar
dc-demo-changelog.jsonl      the history
dc-demo-agent-token          the agent's key       <- 0600

The startup log prints the path, never the token. Send it as a bearer token and the request is allowed the same mutations a signed-in person gets:

TOK=$(cat /path/to/dc-demo-agent-token)
curl -s -X POST http://127.0.0.1:8901/log \
  -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{"events":[{"action":"note","id":"log-ship","to":{"note":"venue confirmed"}}]}'

To change the plan itself, send ops (0.77.0), the same ones a tab sends, and only the fields you mean to change:

curl -s -X POST http://127.0.0.1:8901/ops \
  -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{"ops":[{"op":"row","uid":"00120","set":{"owner":"Noah","e":"2026-11-03"}},
             {"op":"row","uid":"00121","add":{"deps":["fab-board"]}}]}'

Every open tab shows the change at once, and nobody's other edits to those rows are touched. Then post the matching changelog line to /log, as below.

What that buys you is an audit trail instead of a mystery. The write goes through the same handler, the same append path and the same hash chain as everybody else's, so integrity stays ok:true, and every event it produces is recorded under the actor adom-gantt agent. The changelog then reads:

adom-gantt agent labelled "Ship the workcells to DC" "venue confirmed"

The name is forced. An actor in the request body is ignored the same way a person's would be, so an agent edit can never be read back as somebody's own.

The token does not hand out a session or a cookie, and it does not open the app page: GET / with a token returns the sign-in page exactly as it does with nothing, because the token is an API credential and not a seat in the app. A missing or wrong token is a 401 with the same body a request with no session gets.

This is not a secret from anything already running in the container, and does not pretend to be: a local process can read the plan and the token alike. Its job is to make agent edits auditable rather than invisible.

Editing together

Since 0.77.0 the plan saves the way Google Docs does. A tab does not send its copy of the whole plan: it sends what you changed, as small operations ("the owner of row 00147 is now Noah", "row 00210 goes after row 00198", "add the tag Fab to row 00033"). The server applies them in the order they arrive and tells every open tab, so:

  • Two people editing different fields of one row both keep their change. You set the owner while a teammate sets the status: the row ends with both.
  • The same field, at the same moment, goes to the later change, in every tab. If your value is replaced a moment after you set it, you are told who replaced it and what you had ("Ana changed the owner of "Ship the workcells" to Kai just now (you had Noah)"), so nothing is lost without a word.
  • Lists merge item by item. Two people adding a different dependency, or a different tag, to the same row both keep theirs; removing one takes out that one only.
  • Moves merge too. A move is "this row goes after that one", replayed on whatever order the plan has by then, so two people reordering different rows both keep their order.
  • Somebody else's change costs your tab only what it changed (0.78.1). A row somebody drags moves only that row on everybody else's screen, an edit redraws only that row of the table, and the server keeps the plan parsed in memory instead of reading it from disk for every change. On the team plan a tab that is only watching went from about 200 ms per moved row to under 20.
  • A tab that falls out of step puts itself right (0.77.1). If a tab ever disagrees with the server about which section a row is in, it notices on the next change anybody makes, fetches the whole plan and corrects itself. Before 0.77.1 it could not, and on 2026-11-03 two such tabs and a third kept changing the row order back and forth for an hour.
  • Ctrl+Z is yours. It takes back what you did and nothing anybody else did since. If a teammate changed a field after you, your undo leaves their value and the toast says so ("kept Ana's newer owner"); in a list, only the items you added or took out go back; a row you created that a teammate has since worked on stays. Redo works the same way.
  • An edit outlives a deletion it did not see. If a teammate deletes a row while you are changing it, your change brings the row back, and you are told.
  • Nothing to refresh, nothing to clobber. Every tab is current, and a tab that drops off (a laptop lid, a flaky connection) catches up on its own when it comes back.

The red banner at the bottom ("Your changes are not saving") is now only for a save the server could not take at all: the plan stopped being shared with you, your sign-in ended, or the server could not be reached. Your edits stay on screen; Try again sends them again (safe to repeat, because each change says what a field IS), Export my version writes them to a file, and Reload latest takes the server's version after saying how many changes it would discard. A save that failed for want of a server is retried on its own every few seconds.

The changelog cannot run ahead of the plan. Changelog lines are chained to the save that carries them: while your saves are failing, your lines queue in the tab rather than posting, and they go out in order behind the first save that lands, or are dropped along with the edits if you choose to discard them.

Asking someone to take a task (0.80.0)

The point is to keep people informed: work does not land on somebody's plate without them knowing. Set a row's owner to another person (in the Table, the owner pill, the item editor, the New panel, a paste) and the row keeps its owner while a dashed pill after it says who was asked: → Ana?. Ana gets a toast and a count on the Requests button (the tray by your name in the header). Its panel lists:

  • For you: what people have asked you to take, each with an optional note and Accept or Decline. Accept makes the row yours; Decline leaves it as it was.
  • Changes to your tasks: somebody moved the dates of tasks you own. One notice per person per save (a burst of edits in ten minutes is one notice), with the dates each task had.
  • You asked: your requests, pending, accepted or declined, by whom and with their note, and Withdraw while one is pending.
  • Waiting on others, for the plan's owner.
  • Let my AI answer requests for me: an AI with the plan's agent key may then accept or decline in your name, and the history says it was your AI.

The item editor shows a request on its row, with the same buttons. Ctrl+Z takes back your own step: a request you made is withdrawn, an answer you gave is taken back and the request is pending again. Every step is in the changelog and the Chat digest, written by the server ("Drew Owens asked Ana to take "Etch the cards""; "Drew Owens moved the dates of 3 of Ana's tasks, and Ana was told").

What is NOT a request: taking a task yourself, clearing an owner, putting an owner back that you just changed, a name with no Adom account, and another person's task's dates (a notice instead), section or status.

Who is who: People, on Setup. Owners are names. A name is a person when it matches the display name, username or first name of somebody who has signed in to the plan, or when the plan's owner has linked it to their account in the People card. The card lists every owner name, how many rows carry it and its account; it warns when two people share a first name (both are asked until one is picked). A name nobody answers to is assigned directly and gets a dotted underline in the chart and the Table, so it gets linked.

The server enforces it, for every tab, script and AI: a field-level save, a whole-plan /save and the agent token all turn an owner change onto a person into a request. The agent token answers only for a person who allowed it, naming them. A tab from before 0.80.0 sees its owner change bounce back (reload it). Only the plan's owner can turn requests off (POST /access {"requests":"off"}); they are always off under --no-auth, where nobody signs in.

Your view and the shared plan: what lives where

Your view and the shared plan
   Your view vs the plan   
Folds, filters, the lens and the text size stay in your browser, while every row, date and owner lives on the server and is the same for everyone. What lives where sorts each setting into this tab, this browser or the plan's server, with a one-page diagram. Read it before a team review, so you know which of your clicks only change your own screen and which change everyone's.

The diagram and the full list

What lives where: only this tab, this browser, and the plan's server

Your view stays in your browser; the plan is on the server and the same for everyone. What lives where sorts every setting.

Keyboard shortcuts

Key Action
Ctrl+Z Undo
Ctrl+Shift+Z Redo, also Ctrl+Y. Replays only what undo took back, so any new edit clears it
Up / Down Move the selected row in the table, following the visible order under sorts and filters. They never scroll the pane by themselves; it moves only enough to keep the selected row in view
Up / Down in a list Every list of the app's own walks with the arrows (0.69.0): the status pill's and the owner picker's on the chart, the phase, status, type and "waits on" lists in the editors, the Table's cells, and the Export and Import menus. Down and Up move the mark, Home and End jump to the ends, a letter jumps to the next entry starting with it, Enter takes the marked entry, Escape closes the list changing nothing, Tab closes it on the way to the next control
Enter Edit the selected row's name. Inside a cell, commit and step down one row
Delete, Delete Delete the selected rows, in the chart and the Table. The first press marks them in red and asks (the toast names one row, or counts a set); the second press, within four seconds, deletes them, and one Ctrl+Z brings them back. Any other key, a click or Esc withdraws the question. In the Table with nothing selected it acts on the focused row, and the focus then moves to the next row. Only when no field is open, so Delete inside an editor just edits text. Backspace is deliberately not bound
Shift+D Dependency mode: click the task or milestone that comes first, then the one that waits on it. A milestone is picked by its diamond, or in the lines style by the cap on its rule. The Deps button glows in the accent for as long as the mode is on, and its tooltip is the instruction. The row under the pointer lights up across both panes, tinted in its own phase colour, so you can see which task you are about to click. The armed first task wears the normal selection outline. Clicking a pair that already has a dependency removes it. Click an arrow to pick it, then Delete twice to remove that one. Stays on so you can chain them. Esc lets go of a picked arrow, then the pick, then leaves
Shift+N Open the New panel. Fires only when nothing else is going on, so a capital N typed into a field stays a capital N
Ctrl+C Copy the selected rows, tasks and milestones alike, to the app's own clipboard, ready for Ctrl+V. It ALSO puts their permanent IDs and names on the system clipboard, one per line, which is what it has always done and what an AI batch edit reads. Answers only when the app is idle and there is no live text selection on the page, so a Ctrl+C aimed at text you have highlighted, or typed into any field, is the browser's copy and never this
Ctrl+V Paste the copied rows as new rows. With a row selected they go DIRECTLY BELOW it (below the last of a set, in plan order) and into its lane; with nothing selected they go to the end of the lane you are looking at. Dependencies among the copied rows are remapped onto the copies, so a copied chain is a chain; dependencies pointing outside the set still name the originals, and nothing downstream of an original is changed. The whole paste is one Ctrl+Z
Alt+Right / Alt+Left Nest the selected row (or the focused row in the table) under the row just above it, as that task's last subtask, or take it out to straight after the task it is under (0.79.0). Several selected rows nest and come out together, as one undo. Only with a row to act on, so with nothing selected the browser keeps its own Back and Forward
Alt+Up / Alt+Down Move a row one slot, among its siblings since 0.79.0: a task steps past the next task with all of its subtasks, and a subtask stops at the first or the last of its task's subtasks (Alt+Left takes it out). The rest as before: the focused row in the table, the selected task in the chart. Crossing a lane boundary moves the row into that lane, and a lane that is collapsed OPENS the moment the moving row lands in it, so you can see what you are moving among. Under a column sort the row stays inside the run of rows the sort considers tied with it. The pane follows the row it is moving, keeping five rows visible ahead of it in the direction of travel, and both columns stay locked. With several rows selected the whole set moves as a group; if the selection has gaps in it, the FIRST press closes it up into one block at the topmost selected row and every press after that moves the block
Tab / Shift+Tab In the table, walk the selected row's fields. With no editor open it opens the first editable cell (Shift+Tab the last); with one open it commits and moves to the next. Wraps across rows and around the ends, and follows the visible order under a sort. With no row selected, Tab walks the page's own controls as usual. In the edit panels it is one keypress per field the same way, including the date fields, which Tab leaves whole instead of walking their day, month and year segments
Esc Leave the field first: pressed with the caret in any text box it takes the focus back out of it, so the keyboard shortcuts work again on the very next keypress. It does not clear the box, and it does not do anything else in the same press. Fields that already have an Esc of their own keep it (a name editor cancels, a dropdown's type-in closes the list, a panel field closes the panel). With nothing focused it goes on as before: cancel the open field, then clear the row focus, then clear the selection. A row picked with its handle clears in one press
Ctrl+click Toggle item in selection (on a row's handle, a bar, a name or a Table row)
Shift+click Range select, over the rows on screen

The focused row is the row whose cell editor you touched last, marked with an accent bar down its left edge. It is not a separate thing to click: the edit you were already making is what tells Alt+Up and Alt+Down which row you mean.

Interactions

Dragging a bar snaps it to the visible time grid: day boundaries at the day cell size, week starts at weeks, month, quarter and year firsts at the coarser ones. The preview jumps line to line as you cross each midpoint, and what you see is what is committed. Resizing an edge moves that edge only; dragging the body moves the start and keeps the duration. Milestones snap the same way. Dates typed into the table, the item editor or the batch editor are taken exactly as typed.

Gesture Action
Gantt / Table / Analytics / Setup Switch lens. Pinned to the left of the header so it never moves. Setup is where sections are added, renamed, recoloured, reordered and removed, and where tags are made, coloured, renamed and removed
Drag a section row on Setup Move the section; a line shows where it lands. Alt+Up / Alt+Down move a focused row. One Ctrl+Z
Click a section's name or aside on Setup Edit it in place: Enter or click away commits, Escape cancels
Fit Fit the whole plan into the pane, no horizontal scrolling
Auto-schedule Push every task and milestone out to its dependencies, finish to start, skipping weekends and the plan's days off (Setup, Working calendar). A milestone waiting on work lands on the day that work ends, and a task waiting on a milestone starts on its date. Never pulls work earlier, so intentional gaps survive. One undo. Disabled when the plan has no dependencies
Critical Light up the longest run of work inside every section, cancelled work left out. On/off, and it recomputes as the plan changes
A- / A+ Step the text size for both views. Click the percentage to reset
Collapse all / Expand all Fold every section to its header, or open them all. Start of the legend
Click a # tag chip in the legend Show only the rows carrying that tag (or any of the chosen tags), on themselves or through their section, chart and table. Clear tags undoes it. Your view only
Hover "today" The current date
Click a table cell Edit owner, start, deadline, status or notes in place
Click a table header Sort by that column (deadline ascending by default)
Click a row's handle (left edge, on hover) Select the row and nothing else; Ctrl adds or removes, Shift takes the range
Click a bar Select + open the item editor
Click a name Rename it in place
Drag bar edge Resize start or end date
Drag bar body Move bar (preserves duration)
Drag vertically Reorder rows. Between two rows, the drop line is indented to the level the row will land at; move the pointer right to nest it under the row above, left to take it out (0.79.0)
Drag onto the middle of a task Make the dragged rows that task's last subtasks; the task is outlined while you hover it (0.79.0)
Click a task's chevron Fold or unfold its subtasks. Your view only (0.79.0)
+ on a section header, + on a task Quick add: a blank row at the end of the section, or as the task's last subtask. Enter adds and keeps the row open, Esc stops (0.79.0)

Server endpoints

Everything below the door needs a signed-in session, or an Authorization: Bearer <agent-token> header. GETs of /data, /state, /changelog and /export/* over direct loopback are open. /health is public and leaks nothing but counts.

Endpoint Method Description
/health GET Public. Rev, event count, and the changelog's integrity object
/access GET or POST Who can open the plan: `{mode: any
/requests GET The requests (0.80.0), each with its row's name and owner now, the notices of moved dates, the accounts and who lets their AI answer. ?state=pending, ?to=<name>, ?uid=, ?id=. A signed-in caller also gets mine: {for, asked, notices}. Made, answered and withdrawn through /ops
/people GET Every owner name on the plan and the account it resolves to (linked, match, first, shared, none, off), with counts; ?name= for one name
/notify GET or POST Google Chat digests: status (never the webhook), cadence, test message; the owner or the agent token sets the webhook
/live GET Server-sent events: revisions, presence, and denied when the plan stops being shared with you
/live/ping POST ?cid=<tab>: the tab is still open. A stream not heard from in 150 s is dropped from presence
/data GET Current project data
/state GET Saved edits (sidecar), with requests and people
/save POST Persist edits
/uid POST Allocate the next permanent item id. {"count":n} for a batch, up to 500
/uid/backfill POST Give a uid to any row that has none. Runs at startup too, so this is only for a data file edited under a running server
/changelog GET Edit history. ?since=<rev> for the tail, ?format=md for the readable version
/log POST Append events to the changelog (used by the app, and by an AI recording its own edits)
/changelog/reset POST Delete the changelog
/import/json POST Import native JSON
/import/csv POST Import CSV
/import/msproject-xml POST Import MS Project XML
/import/jira-csv POST Import Jira CSV
/import/clickup-csv POST Import a ClickUp CSV export (a view's or the workspace's). ?space=, ?folder=, ?list= keep part of it; ?today=YYYY-MM-DD is the day undated tasks land on (the page sends its own). Answers with a report
/import/clickup-json POST Import the ClickUp API's answer for a List's tasks, saved to a file. Answers with a report
/import/clickup-api GET or POST POST `{"list"
/compact POST Bake saved edits into the data file and empty the sidecar
/export/json GET or POST Export merged JSON. GET renders the plan on disk (the AI/loopback loop); POST renders the client model in the body, which is how the Export button reflects unsaved edits
/export/csv GET or POST Export CSV, same GET/POST split
/export/svg GET or POST Export SVG, same GET/POST split
/export/html GET or POST Export self-contained HTML, same GET/POST split

Examples

Two example datasets are included:

  • examples/sample-product-launch.json: 4-phase hardware product launch (26 items, 4 milestones, dependency graph)
  • examples/smt-line.json: 5-phase SMT production line build (43 items, 8 milestones)

Tests

A browser suite drives the real app in headless Chrome against a seeded 20-section plan on a scratch port: every behaviour above that 0.57.0 touched, a control-coverage gate that fails on any control nobody declared, a phone and two desktop widths, and a page-error gate on every page.

cd test && npm install        # puppeteer-core, once; Chrome from ~/.cache/puppeteer
node test/ui_test.mjs         # exits non-zero on any failure
node test/ui_test.mjs --keep --plan path/to/plan.json   # also a real plan (copied first); keep screenshots in test/out/
node test/ui_test.mjs --only "editor: item,text size"   # a subset of flows, for a fast inner loop
node test/server_test.mjs --only clickup                 # the server suite's ClickUp section alone

The ClickUp import is covered by synthetic files in ClickUp's real shapes, in test/fixtures/: a workspace export written from GMT+5:30, a Folder's view export with ISO dates, and the API's answer for a List. The server suite also runs the live API against a stand-in for ClickUp on loopback; the browser suite's clickup import flow imports through the menu and reads the report.

Several people at once (0.75.1, field-level since 0.77.0) has a suite of its own, test/collab_test.mjs: real Chrome tabs, each signed in as a different person through a stand-in for Adom sign-in, on one plan shaped like a real launch plan (about 490 rows, 50 sections, 190 dependencies). It plays a meeting: people join one after another, edit their own rows, look at rows, scroll and flip their own filters, all at once. It measures what each tab pays (main-thread time, the longest freeze, forced layouts, memory) and checks that every tab ends up holding exactly what the server holds with no edit lost. It also covers a save seen from every lens, presence, idle cost (critical path included), a 150-save soak for leaks, two people on one row, and a tab that drops off and catches up. Since 0.77.0 a docs scenario checks each Google-Docs-style behaviour above with two signed-in people, and a fuzz scenario has up to six people (--users) edit the same rows as fast as they can (fields, lists, moves, undo and redo, tabs dropping off and coming back), then checks that every tab holds exactly what the server holds and that no field only one person wrote was lost (--seed picks another random sequence). Since 0.78.0 a milestones scenario has two people link the same milestone at the same moment, in both directions, and checks that both links stand everywhere.

node test/collab_test.mjs                      # 6 people, every scenario, about 6 minutes
node test/collab_test.mjs --users 12 --throttle 4   # a bigger meeting on slower laptops
node test/collab_test.mjs --plan my.json --report out.json   # a real plan, every number saved

A full run of the browser suite takes about 12 minutes (14 with --plan). Since 0.57.1 the suite also opens every editor and menu, sweeps the header at 54 widths in every lens (four since 0.58.0), fails on any failed request to the app's own server, and fails when a control marked safe was never clicked. Controls with no test are marked untested with a reason and counted in the summary, so the gap is stated and not hidden.

Collaboration

Open the same plan from any number of tabs or people (every reader signs in with Adom). It works the way a Google Doc does (0.77.0): each change goes to the server as the one field it touched, every open tab shows it within about half a second, and two people never lose each other's work by touching the same row. Ctrl+Z takes back only your own changes. The legend shows who else has the plan open and which rows they are on. See Editing together.

It stays light with a room full of people (0.75.1). Another person's save costs an open tab about a fifth of a second on the chart on a current laptop, and under a second in any lens; someone clicking around costs the others next to nothing; and an idle tab costs nothing, critical path or not. Through 0.75.0 every save anyone made cost every other open tab 15 to 25 seconds of one CPU core: the arrows were redrawn once per bar, about 480 times, and the rows the sidecar added were placed one full layout at a time. Six people in one meeting froze each other's laptops that way. test/collab_test.mjs holds the line. Use Export (JSON) to hand a copy around and Import (JSON) to load one; an import replaces the plan and reloads every other open tab.

License

Internal tool. Adom Inc.