P3-5: Status, activity and conflict UI #34

Open
opened 2026-09-10 17:44:15 +00:00 by Cordy · 1 comment
Owner

Depends on P3-4.

Goal

Make the engine's state legible. Everything below already exists in sync.Result — this task is about surfacing it, not computing it.

Files

  • Create: internal/app/status.go, frontend/status.*, frontend/conflicts.*

Main window

  • Status header: "Up to date" / "Syncing 3 of 12" / "Paused" / "2 items need attention", with last-sync time and free space (from the RFC 4331 quota properties fetched in P2-2).
  • Recent activity: last ~50 operations, each with path, direction and time. Users check this to confirm the thing they just saved actually went.
  • Attention list: the union of Result.Skipped and Result.Conflicts. This is the important one and must never be a silent counter.

Conflicts

For each conflict, show both files with size and modification time, and offer three actions:

  • Keep mine — the conflict copy replaces the canonical path.
  • Keep theirs — delete the local conflict copy.
  • Keep both — leave as-is; this is already the state on disk, so the action is really "dismiss".

Both files already exist locally, so no action here can destroy data — which is exactly why the conflict policy was designed that way in Task 12. Say so in the UI: "Both versions are saved. Choose which one keeps the original name."

Skipped items

Each skip carries a Reason from the engine (illegal filename, case collision, quota, partial write). Show the reason verbatim — those strings were written to be read by users, not developers. Where the fix is obvious, say it: a Windows-illegal name should suggest renaming, and offer to open the containing folder.

Quota exhaustion

Give it a dedicated state, not a generic error: "Your Cairn is full. Nothing is being uploaded until space is available." Link to the web UI. Never present this as a transient failure the user should wait out.

Notifications

Native OS notifications, used sparingly: first successful sync, a new conflict, quota exhausted, auth expired. Never notify on routine success — that is how an app gets muted permanently.

Steps

  • Build the status, activity and attention views.
  • Implement the three conflict resolutions.
  • Wire native notifications with the restraint rules above.
  • Verify every Skip.Reason renders legibly for a non-technical reader.
  • Commit: git commit -s -m "feat(app): status, activity and conflict resolution UI"

Acceptance criteria

  • Every conflict and skip is visible and actionable; none is silently swallowed.
  • No conflict resolution can lose data.
  • Quota has its own distinct, non-transient state.
Depends on P3-4. ## Goal Make the engine's state legible. Everything below already exists in `sync.Result` — this task is about surfacing it, not computing it. ## Files - Create: `internal/app/status.go`, `frontend/status.*`, `frontend/conflicts.*` ## Main window - **Status header:** "Up to date" / "Syncing 3 of 12" / "Paused" / "2 items need attention", with last-sync time and free space (from the RFC 4331 quota properties fetched in P2-2). - **Recent activity:** last ~50 operations, each with path, direction and time. Users check this to confirm the thing they just saved actually went. - **Attention list:** the union of `Result.Skipped` and `Result.Conflicts`. This is the important one and must never be a silent counter. ## Conflicts For each conflict, show both files with size and modification time, and offer three actions: - **Keep mine** — the conflict copy replaces the canonical path. - **Keep theirs** — delete the local conflict copy. - **Keep both** — leave as-is; this is already the state on disk, so the action is really "dismiss". Both files already exist locally, so no action here can destroy data — which is exactly why the conflict policy was designed that way in Task 12. Say so in the UI: *"Both versions are saved. Choose which one keeps the original name."* ## Skipped items Each skip carries a `Reason` from the engine (illegal filename, case collision, quota, partial write). Show the reason verbatim — those strings were written to be read by users, not developers. Where the fix is obvious, say it: a Windows-illegal name should suggest renaming, and offer to open the containing folder. ## Quota exhaustion Give it a dedicated state, not a generic error: **"Your Cairn is full. Nothing is being uploaded until space is available."** Link to the web UI. Never present this as a transient failure the user should wait out. ## Notifications Native OS notifications, used sparingly: first successful sync, a new conflict, quota exhausted, auth expired. Never notify on routine success — that is how an app gets muted permanently. ## Steps - [ ] Build the status, activity and attention views. - [ ] Implement the three conflict resolutions. - [ ] Wire native notifications with the restraint rules above. - [ ] Verify every `Skip.Reason` renders legibly for a non-technical reader. - [ ] Commit: `git commit -s -m "feat(app): status, activity and conflict resolution UI"` ## Acceptance criteria - Every conflict and skip is visible and actionable; none is silently swallowed. - No conflict resolution can lose data. - Quota has its own distinct, non-transient state.
Author
Owner

Amendment — 2026-09-11: an empty sync folder is its own state, never a delete (phase-2 final review X2)

Final review X2 found that a pass whose local folder lists nothing, while the state store holds
rows, deletes every server copy, and so every other device's copy. That happens, for instance, when
the sync folder is on a disk that is not mounted. The same holds the other way round: a server
folder that lists nothing deletes every local file. The fix adds an engine refusal. Such a pass
returns an error naming the empty side and applies nothing, unless the caller explicitly allows it.

This UI must surface that refusal as a distinct attention state, never as a generic or transient
error:

  • Local side empty: "Your sync folder is empty, but N files were synced from it. Is its drive
    connected?" The default action is to wait, and the next pass checks again. The destructive action,
    "Delete them from your Cairn and your other devices", needs an explicit confirmation that states
    the number of files.
  • Server side empty: "Your Cairn folder is empty on the server, but N files were synced to it."
    Offer "Upload them again from this device", which forgets the state rows so that the next pass
    merges the two sides and uploads, and "Delete them here too", which needs the same explicit
    confirmation.
  • Send a native notification, as for quota exhausted and expired sign-in. Never proceed
    automatically, and never retry into the deletion.

Acceptance: no path through this UI deletes files on the empty-side refusal without that explicit
confirmation.

## Amendment — 2026-09-11: an empty sync folder is its own state, never a delete (phase-2 final review X2) Final review X2 found that a pass whose local folder lists nothing, while the state store holds rows, deletes every server copy, and so every other device's copy. That happens, for instance, when the sync folder is on a disk that is not mounted. The same holds the other way round: a server folder that lists nothing deletes every local file. The fix adds an engine refusal. Such a pass returns an error naming the empty side and applies nothing, unless the caller explicitly allows it. This UI must surface that refusal as a distinct attention state, never as a generic or transient error: - **Local side empty:** "Your sync folder is empty, but N files were synced from it. Is its drive connected?" The default action is to wait, and the next pass checks again. The destructive action, "Delete them from your Cairn and your other devices", needs an explicit confirmation that states the number of files. - **Server side empty:** "Your Cairn folder is empty on the server, but N files were synced to it." Offer "Upload them again from this device", which forgets the state rows so that the next pass merges the two sides and uploads, and "Delete them here too", which needs the same explicit confirmation. - Send a native notification, as for quota exhausted and expired sign-in. Never proceed automatically, and never retry into the deletion. Acceptance: no path through this UI deletes files on the empty-side refusal without that explicit confirmation.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-desktop#34
No description provided.