master
Drew Owens Publish 0.77.0 48f13b8

adom-gantt collaboration: a plan

Date: 2026-10-06. Requested by Drew: "add collaboration / shareable features ... Must avoid constantly clobbering each other's updates on refresh." Not started; waiting on the decisions at the end.

Where it stands today

The app already has the pieces a multi-user plan needs, but it saves like a single-user one:

  • Sign in with Adom gates every page load and every change; the session is a 30-day cookie; the changelog records the signed-in person as the actor of each edit.
  • Saving is whole-state, last-write-wins. Every edit, 800ms later, POSTs the ENTIRE sidecar (every row's overrides, the section list, the order, the registry). The server keeps a stateRev; a save carrying an older baseRev is refused with 409.
  • A refused tab is blocked. The red banner offers Export my version, Save mine anyway (force-overwrite) and Reload latest (discard your work). There is no fourth option, because the server has no way to combine two whole states.
  • Nothing is live. A tab learns the plan changed only when its next save is refused. A refresh gets the server's state and drops whatever the tab had not saved.

So with two people in the plan, the second person's first save after the first person's is refused, and the only way out is to throw away one side's work or to force-write over the other's. That is the clobbering.

The one idea that fixes it

Save what changed, not the whole plan, and tell every open tab.

An edit becomes an op: a small, field-level patch such as {op:'set', uid:'00042', field:'end', value:'2026-11-03'}, {op:'move', uid, before}, {op:'phase.rename', key, name}, {op:'tag.color', ...}. The client already produces exactly this information for the changelog (diffAndLog), so the op stream is the changelog's event stream made authoritative rather than descriptive.

  • The server applies ops, in arrival order, onto the sidecar it holds, bumps the rev per op, appends them to the changelog (one write, one hash chain, no second store), and broadcasts them to every other open tab over a server-sent-events channel.
  • Tabs apply remote ops to their in-memory plan and repaint the row that changed, the way an undo already repaints one row. A tab is never stale, so a refresh never has anything to clobber: the state it reloads is the state it already showed.
  • Two people editing the same plan no longer collide, because they are editing different fields of different rows almost all of the time, and field-level ops to different fields simply both land. The only true conflict is two people changing the SAME field of the SAME row within seconds of each other; the later op wins that field and the loser's tab shows a quiet notice ("Ana changed the deadline of X just now"), with the value it tried in the notice so nothing is lost silently.
  • Undo stays personal. Ctrl+Z undoes your own ops, as ops, so it cannot undo somebody else's work and cannot be undone by theirs.

No locking, no merge dialogs, no "who has the plan open". This is how every shared document tool behaves and it is what people expect.

What ships, in order

Each checkpoint is shippable alone and goes out as its own version, lean process (targeted tests, publish, full suite in the background).

Checkpoint 1 (0.66.0): live tabs, no more surprise 409s. BUILT 2026-10-07, see BUILD-NOTES

The smallest change that ends the clobbering, built on the current whole-state save:

  1. A live channel (GET /live, server-sent events). The server sends the new stateRev and the actor whenever a save lands, plus a heartbeat.
  2. A tab that hears a newer rev pulls the state and merges it row by row: rows the tab has unsaved edits on keep the tab's values, every other row takes the server's. Then the tab saves, and its save carries the new rev, so it is not refused.
  3. Presence: a strip at the right of the legend showing who has the plan open (initials, the name on hover). "Ana is editing" against a row was left for checkpoint 2, where the op stream can carry it; the live channel is one-way.
  4. The red banner stays only for the case the merge cannot handle: a save refused twice in a row, which means something is wrong with the connection, not with the plan.

This does not need the op model and takes an afternoon. It removes the refresh problem for everyone, because every tab is current before it saves.

Checkpoint 2: ops, the real fix. BUILT 2026-10-30 as 0.77.0, see BUILD-NOTES

Built as described below, with three changes found while building: list fields (dependencies, tags) merge item by item rather than whole; a move is an op of its own ("after that row"), replayed on the order as it stands; and the whole-state /save stays for old tabs and scripts rather than for Auto-schedule, which sends its rows' fields as ops like any edit. Undo is per field and personal, as planned. The original text follows.

Saves become field-level ops (the shape above), applied and broadcast by the server. The whole-state /save stays for imports, the project-start shift and Auto-schedule (which touch every row and are one op each: plan.shift, auto-schedule, import). The changelog becomes the op log's storage; ?format=md is unchanged. The same-field notice appears. The agent token writes ops too, so an AI editing the plan and a person editing it no longer collide either.

(0.68.0 went to the sharing SETTING and the org gate instead, see BUILD-NOTES 2026-10-14; the links below are still planned, after field-level ops.)

  1. Share a link to a row: every row has a URL (#row=00042); opening it scrolls to the row, selects it and opens its editor. The item editor gets a "copy link".
  2. Share a view: the URL carries the lens, the grain, the tag and owner filters and the collapse set when you copy it from the share menu, so "look at the Ops tasks due this month" is one link. Opening it applies that view without changing the reader's saved preferences.
  3. View-only share link: a long random token that lets someone without an Adom account read the plan, live, with every edit control hidden. Created and revoked on the Setup page; listed there with who made it and when it was last used. Nothing on a view-only link can write.
  4. Publish to the wiki from the Export menu: the snapshot export the other agent runs by hand becomes one click, and the page it lands on is remembered per plan.

Checkpoint 4 (0.69.0): seeing each other's work

  1. Who changed what: hovering a row's bar or name shows "Ana, 2 hours ago" from the changelog; the Table gets an optional Updated column.
  2. Since you were here: on load, a one-line summary ("Ana made 6 changes since Tuesday") with a link that opens the changelog filtered to them, and the changed rows outlined for a few seconds.
  3. Comments on a row: a thread per row in the item editor, with @mentions of people in the plan. Stored as changelog events (comment), so they are attributed and append-only like everything else. Mentions could later notify through Google Chat, which is out of scope here.

What is deliberately NOT in this plan

  • Real-time cursors or character-by-character co-editing of a text field. A plan edit is a date, a status, an owner, a name: whole values, so op-level is enough.
  • A hosted multi-tenant service. The app stays one server per plan, in whoever's container runs it, reached through the Adom proxy with Sign in with Adom.
  • Permissions beyond signed-in-can-edit and view-only-link-can-read. Per-row ownership rules would be a separate decision.

Risks

  • Checkpoint 1 merges by row. Two people editing the same row at the same minute still ends with one of them refused for that row; checkpoint 2 fixes it. Said plainly in the status note when 0.66.0 ships.
  • The agent token. Until checkpoint 2, an AI writing to the plan writes whole state; after a person's edit, its save would be refused. Today it already is.
  • Snapshots (the wiki page) stay read-only copies and do not get live sync; they are files.
  • Tests. The suite is single-tab; checkpoints 1 and 2 need two-tab flows (the suite already opens two pages in one browser for the text-size test, so the pattern exists).

Decisions (Drew, 2026-10-07)

  1. Field-level ops, last writer wins per field. Agreed.
  2. Reach. CORRECTED 2026-10-14. The 2026-10-07 note here was wrong on two counts. The /proxy/<port>/ URL sits behind the container OWNER's Adom login, so teammates cannot open it (and sharing it would expose every port on the machine). And the workspace this app was being built and previewed in is not a cloud container at all: it is the local Hydrogen workspace on Drew's laptop (slug: hdlocal), which has no cloud slug to map a port on and goes down whenever the laptop sleeps. See "Sharing with the team" below for what reaches a team.
  3. Confidential: every reader signs in with an Adom account. No anonymous links. Per plan, a sharing setting on the Setup page, enforced server-side on every request:
    • Only me: the owner (the account that set it, or the first signed-in account).
    • My org: members of a chosen org (membership read from Carbon on sign-in and refreshed per session).
    • Everyone with the link: any signed-in Adom account. Each level has a read and an edit side (for instance: org can edit, everyone with the link can view). The default for an existing plan is "my org can edit", and the Setup page says what it is. View-only readers see every control for writing hidden and the server refuses their writes.
  4. Comments on rows: yes, with a "show / hide comments" switch on the Setup page.
  5. Order as planned. Checkpoint 1 next.

Checkpoint 3 changes accordingly: the "view-only token link" item is replaced by the per-plan sharing setting above (no tokens; identity is always the Adom session).

Sharing with the team, and changes posted to Google Chat (recommendation, 2026-10-14)

Drew: "share a gantt project with the team ... all have full control of the app ... see changelogs coming in regularly (maybe real time, or polling every few minutes or every hour?) to a private gchat space via webhook."

Where the shared plan runs: its own small cloud container

  • A dedicated Adom cloud container (class small), owned by Drew, running only adom-gantt for the shared plan(s), installed from the wiki like anywhere else.
  • adom-cli carbon containers port-add <slug> --port <port> --prefix gantt gives it a permanent https://gantt-<random>.adom.cloud address. That address is public at the network level by design; the app's own Sign in with Adom is the gate on every request (page, data, saves, the live channel), which is why the org gate below comes first.
  • Supervised the way the production Adom services are (the standalone-service pattern: started by the container's entrypoint, a watchdog every 2 minutes restarts it), so it survives a crash and a restart.
  • NOT Drew's laptop workspace (offline whenever the laptop sleeps, no port mapping), and NOT the container ACL (adding anyone to a container's ACL permanently disables its session token; teammates need the app, not the machine).
  • Backups: the plan, sidecar and changelog are copied to a private wiki page nightly, and before every import, so a bad import or a lost container is a restore, not a loss.
  • To verify while building: whether an idle cloud container sleeps (the docs say a sleeping container returns 502 until it wakes), and how to keep this one awake.

Who gets in: everyone in the Adom org, with full control

  • At sign-in the server reads the person's org memberships from Carbon (GET /user/memberships, with the token it already holds for that moment) and keeps only the org names in the session.
  • A per-plan setting on the Setup page: Only me / My org (adom) / Anyone signed in to Adom. Everyone it admits has full control: every edit, every lens, Setup, import and export. Anyone else who signs in gets a page saying the plan is not shared with them.
  • Two things stay with the plan's owner, because they are a credential and the lock on the door: the sharing setting itself, and the Chat webhook.
  • Before inviting the whole team: field-level ops (checkpoint 2). Today two people editing the SAME row within a save of each other lose one side's change without a word; rare for one person in two tabs, likely for a team.

Changes into a private Google Chat space: pushed by the server, as digests

  • A Chat incoming webhook on a private space (Drew creates the space and adds the webhook in Chat: Apps & integrations, Webhooks). Messages arrive from the webhook's own name ("Gantt"), not from Drew. The URL is a secret: stored on the server in a 0600 file, set by the owner, never sent to the page or the wiki.
  • Pushed by the gantt server itself, not polled. Every change already goes through one append path into the changelog, so the server knows the instant something changed; a poller on another machine would need that machine awake and would only ever be slower.
  • Digests, not one message per change. A drag is one change; a batch edit or an auto-schedule is dozens. So the server collects changes per person and posts when that person has been quiet for 2 minutes, or every 10 minutes during continuous editing: "Ana · 6 changes · : moved X +3d (12 Oct to 15 Oct); assigned Y to Kai; ... and 3 more", with links to the plan and to the changelog since the last post. Imports, plan shifts and Auto-schedule get one line each, the way the changelog already collapses them.
  • One Chat thread per plan per day (the webhook's threadKey), so the space reads as a daily log rather than a wall.
  • Nothing lost, nothing doubled. The last posted changelog revision is kept on disk; a failed post is retried with backoff and a restart resumes from it. Chat allows about one message a second per space, which digests stay far below.
  • Cadence is a setting: near real time (the default above), hourly, or a daily summary; any of them can be turned off without touching the webhook.
  • This half does not need the team or the cloud container: it works on any adom-gantt server, so it can run against Drew's own plan from day one.

Proposed order

  1. Chat digests (0.67.0): usable immediately, on whatever server Drew uses today. BUILT 2026-10-14.
  2. Org gate and the sharing setting, owner-only settings, backup before import (0.68.0). BUILT 2026-10-14.
  3. The cloud container, the port mapping, nightly backups, the plan moved over; Drew uses it first, alone.
  4. Field-level ops (checkpoint 2), then decide whether to invite the team.