Sync history & pending changes
Desktop app only. The desktop keeps a local mirror of your projects and syncs it with the cloud; these are the surfaces where you can see that sync working and steer it by hand. They live under Profile → Cloud sync (the web build shows a "desktop only" message there) in three sub-tabs — Projects, Pending, Activity — plus the Sync modal opened by Sync now or the menubar's N to push pill.
This page is the behavior reference for Pending, Activity, the modal's preview/apply flow and the dedupe endpoint. For what Projects does, see Cloud sync (Profile); for the engine underneath (delta pulls, the shadow table, conflict holding), see Offline sync.
Pending — your unsynced local changes
A staging area listing every annotation or draft on this device that the cloud doesn't have yet: rows edited or created locally, and rows deleted locally whose delete hasn't been sent. The header counts them (N unsynced changes on this device); the list refreshes every time the window regains focus. Rows that lost their sync record are repaired on load so they show up here instead of vanishing.
Each row shows:
- the operation — New (never pushed), Edited (has a cloud copy), Deleted (local delete pending);
- the project title and task number;
- Annotation or Draft, and either cloud #id or local-only;
- a one-line content summary — the first labels / choices / text of up to three regions (+N more), or N region(s), or empty;
- Last sync error, when the last push attempt failed;
- how long ago it was last edited here.
Per-row actions:
- Push — starts a push run for your pending work right away. Note the run is a normal push, not limited to that single row: other pending rows that aren't held as conflicts or in a Manual-mode project move with it.
- Revert (rows with a cloud copy) — after a confirm, discards the local edit and re-pulls the cloud's version of that task's annotations (or drafts). For a pending Deleted row it re-creates the local row from the cloud. Message: Reverted to cloud version.
- Discard (local-only rows) — deletes the local row and its sync record outright; nothing is sent anywhere. Message: Local-only change discarded.
Revert can refuse: cannot revert — local task is missing; sync first (the task mirror is gone), task has no cloud_id, the cloud being unreachable, or the stored cloud token being rejected (sign in again from the Cloud sync tab). Reverting cannot be undone.
Rows held with a "cloud copy changed since last sync — resolve in Sync review" error are conflicts; they're resolved in the Sync modal, not here.
Activity — what synced, when
Two cards driven by the same status snapshot the Sync now button refreshes (so the tab updates each time you sync):
- Sync activity — a chart of rows processed per run over time, pulls and pushes on separate axes, with failed runs flagged.
- Recent syncs — the last 20 runs, newest first, paged 6 per page. Each row: PULL or PUSH, rows processed (and N failed), a per-type breakdown (users, projects, tasks, annotations, drafts), any error text, and a relative time.
Runs come from the autopull/autopush loops, per-project downloads, post-sign-in pulls, and Sync modal applies (recorded as a push run). Empty until the first sync: No sync activity yet — hit Sync now to start.
The Sync modal — preview, then apply
Opening the modal lands on a per-project overview (local knowledge, works offline) with Push / Pull / Review per project and a Manual toggle; nothing moves from the overview without a deliberate click — see Cloud sync (Profile) for that screen. Review runs the preview/apply handshake for one project:
Preview
A dry run against the cloud that classifies every annotation in scope as unchanged, to push (only the local side changed, or the row was never pushed), to pull (only the cloud side changed, or the cloud row is new to this device) or a conflict (both sides changed since the last sync). Only tasks that hold local annotations are walked — a brand-new cloud annotation on a task you have no annotations for arrives with the next delta pull instead.
Preview writes nothing to the cloud, but it does tidy local sync state:
- annotations missing a sync record get one (and show as to push);
- a local row whose content is byte-identical to its cloud copy has its stale "pending" flag cleared on the spot, so it stops counting as pending;
- a locally created annotation that an earlier push uploaded but never linked back is adopted by content match, instead of appearing as both to push and to pull.
While a background sync is already running, the modal shows its live row count instead of a (racing) preview.
Resolutions
To push and to pull rows flow through automatically as compact line items. Each conflict gets a side-by-side Local / Cloud summary and one of four choices:
| Choice | Effect |
|---|---|
| Local | push the local version, overwriting the cloud copy |
| Cloud | pull the cloud version, overwriting the local copy |
| Both | keep the cloud copy as is and push the local one as a new annotation — you end up with two rows on the cloud |
| Skip | leave both sides untouched for now |
Apply is disabled until every conflict has a choice (Resolve conflicts to continue); with nothing in scope the button reads Nothing to sync.
Apply
Executes the plan row by row and records one push run in Activity. Per-row failures don't abort the run — they're counted (N failed, with the last error) on the Sync complete screen. The menubar pending count re-polls immediately. Cloud unreachable or a rejected token abort the whole apply with a clear message; your local changes stay put.
Dedupe cloud annotations
POST /api/afri/sync/dedupe/ (optional body
{"cloud_project_id": <id>}) is a one-shot repair for cloud
annotations that earlier automatic pushes duplicated. For every
mirrored task in scope it fetches the task's cloud annotations,
groups them by content, and for any group with more than one member
keeps the oldest and deletes the rest on the cloud. It returns
duplicates_removed and cloud_annotations_inspected.
There is no button for it — it's a support tool, called from the desktop with a signed-in session. Because it deletes on the cloud, scope it to one project and run the Sync modal's preview first to see what the cloud holds.
Gotchas
- Pending ≠ failed. A row in Pending is simply not uploaded yet; it only indicates a problem when it carries a Last sync error or the conflict hold.
- Manual-mode projects never move on their own — their rows sit in Pending until you Push from the Sync modal.
- Activity keeps 20 runs. Older runs roll off; the operator-side
raw state at
/api/afri/sync/debug/is the deeper view. - Mobile has none of this. The mobile app has an outbox with menubar pills and the Offline storage page; there's no run history or three-way merge there.
What's next
- Offline sync → — the engine: SyncMeta, delta pull, conflict holding, error codes
- Cloud sync (Profile) → — the Projects tab, device-only accounts, account linking
- Offline storage → — the mobile app's on-device store