Skip to content

Sync prune: refresh_reference_data removes server-side deletions + projects:changed event#30

Merged
mariomeyer merged 4 commits into
mainfrom
fix/refresh-prunes-deleted-reference-data
Jun 17, 2026
Merged

Sync prune: refresh_reference_data removes server-side deletions + projects:changed event#30
mariomeyer merged 4 commits into
mainfrom
fix/refresh-prunes-deleted-reference-data

Conversation

@mariomeyer

Copy link
Copy Markdown
Member

Summary

Two bugs the user hit after reorganizing projects in Solidtime:

  1. Stale projects/tasks/clients/tags lingered locally. refresh_reference_data only upserted — it never pruned. Solidtime-side deletions were invisible to stint.

  2. Even with the prune, the picker still showed stale data until app relaunch. No event told the UI to refetch after a reference refresh.

Changes

Backend (stint-core):

  • Reference::archive_projects_not_in / archive_clients_not_in — soft-archive (set archived = 1) any local row whose id isn't in the remote response. Picker hides archived rows; historical entries still resolve the project / client name.
  • Reference::delete_tasks_not_in / delete_tags_not_in — hard-delete (no archived column on those tables). Entries with dangling task_id show no task name; acceptable.
  • refresh_reference_data now calls the prune helpers after each upsert.
  • Resurrection case: if a remote row reappears after a prior local archive, the upsert's ON CONFLICT … SET archived = excluded.archived un-archives it on the same tick. (Regression test.)

Tauri layer (stint-app):

  • New EVENT_PROJECTS_CHANGED = "projects:changed" constant.
  • commands::sync::sync_now emits it after a successful refresh_reference_data.
  • commands::projects::refresh_projects emits it too (and gains an AppHandle<R> parameter).
  • projects_commands.rs tests pass handle.clone() through.

UI (ui/src):

  • Popover.tsx, TimerCard.tsx, EditEntryDialog.tsx listen for projects:changed and call refetch() on their projects (and tasks) resources.

Test plan

  • CI: 20 cargo binaries green (3 new tests in sync_refresh.rs)
  • CI: 271 vitest tests still green
  • CI: clippy + fmt clean
  • Manual verified by user: reorganize projects on Solidtime → click Sync now in stint → picker reflects new state immediately, no relaunch.

Compatibility with #29

PR #29 (ProjectTaskPicker) touches the same UI files. Whichever PR merges first, the second will need a 3-way merge resolution in Popover.tsx, TimerCard.tsx, EditEntryDialog.tsx — both PRs add similar but distinct edits (this PR adds the event listener; #29 swaps in the new combined picker). I'll handle the rebase when the time comes.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 024c6a1469

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Notify UI surfaces that consume project/task lists so they
// refetch and reflect any server-side additions, edits, or
// (after the prune fix) archives.
let _ = app.emit(EVENT_PROJECTS_CHANGED, 0u32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refetch every mounted project/task consumer after sync

When sync_now refreshes metadata while the Today view is already mounted, this event updates TimerCard but not EntryList: ui/src/components/EntryList.tsx:14-35 loads its project and task resources only once. A Solidtime-side project rename or task deletion therefore leaves the labels on existing entries stale until the list is remounted, even though the picker updates immediately. The same omission affects the mounted calendar labels in ui/src/components/CalendarSection.tsx:76-94. Subscribe those consumers to projects:changed and refetch both resources where applicable.

Useful? React with 👍 / 👎.

Comment on lines 94 to +98
refresh_reference_data(&store, &client).await?;
// Notify UI surfaces so the picker reflects the refreshed list
// immediately, including any locally-archived rows the prune step
// produced.
let _ = app.emit(EVENT_PROJECTS_CHANGED, 0u32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace Spotlight slices after pruning reference data

When a refresh archives a removed project or deletes a removed task, this path only emits the frontend event. I checked the repo-wide ProjectsReplaced and TasksReplaced uses: the only Spotlight slice replacement is in crates/stint-app/src/pull_worker.rs:57-67, so refresh_projects can leave deleted items available through Spotlight until a later successful pull tick. This is especially visible after the Settings-triggered refresh and can persist if pulls are not configured or keep failing. Notify the indexer with the reconciled project and task lists here as well.

Useful? React with 👍 / 👎.

mariomeyer added a commit that referenced this pull request Jun 17, 2026
…fresh

Two Codex P2 flags on PR #30:

1) projects:changed reached TimerCard / Popover / EditEntryDialog but not
   EntryList (project + task labels on each row) or CalendarSection
   (project pills on calendar events). Result: after Solidtime-side
   rename or deletion the picker updates but mounted lists kept showing
   stale labels until remount. Both now subscribe + refetch.

2) Sync now (and refresh_projects) only emitted the frontend event;
   they didn't replay the Spotlight slices. pull_worker.rs does it on
   its 5-min tick, so deleted projects/tasks lingered in Spotlight
   results until a successful pull tick. New crate-private helper
   commands::sync::replace_spotlight_slices() mirrors pull_worker's
   notify_indexer(ProjectsReplaced) + notify_indexer(TasksReplaced)
   block. Called from both refresh paths.

EntryList.test.tsx gains the @tauri-apps/api/event mock alongside the
existing ones added in 5396157 — same root cause: jsdom's window has
no __TAURI_INTERNALS__ so any listen() call rejects.
Pre-fix: refresh_reference_data only upserted projects/clients/tasks/
tags returned by Solidtime. Anything deleted server-side lingered
locally forever, so the picker showed stale projects after the user
reorganized on Solidtime.

Fix: after each upsert, prune the local rows whose ids aren't in the
remote response.

  - projects + clients: soft-archived (archived = 1). Historical time
    entries can still resolve the project / client name via JOIN; the
    picker hides archived rows.
  - tasks + tags: hard-deleted. Neither has an `archived` column and
    adding one for this is overkill — entries with a dangling task_id
    just show no task name.

If the user resurrects a project on Solidtime (archived: false again),
the upsert's ON CONFLICT … SET archived = excluded.archived already
un-archives it locally on the same refresh tick.

Tests:
- new: refresh_reconciles_remote_side_deletions
- new: refresh_does_not_re_archive_a_resurrected_project
- existing happy-path test still green
After the prune fix, refresh_reference_data archives projects/clients
locally when they're gone from Solidtime. But the UI surfaces caching
the project list (Popover, TimerCard, EditEntryDialog) had no way to
know they should refetch — so the user clicked "Sync now" and still
saw stale projects in the picker until they relaunched the app.

Fix: emit a new "projects:changed" Tauri event from both
commands::sync::sync_now and commands::projects::refresh_projects
after refresh_reference_data succeeds. UI surfaces that own a
projects/tasks createResource now listen and call refetch on the
event.

refresh_projects signature gains AppHandle<R> as its first parameter
(matches the rest of the emit-capable commands). projects_commands.rs
tests updated to pass handle.clone() through.

End-to-end:
  User clicks Sync now
    → drain_once
    → refresh_reference_data (upsert + prune)
    → app.emit("projects:changed")
    → Popover/TimerCard/EditEntryDialog listeners fire refetch
    → picker shows current state.
…sumers

After adding listen('projects:changed') to TimerCard / EditEntryDialog /
Popover, vitest's coverage run hit 'TypeError: Cannot read properties
of undefined (reading transformCallback)' from the real @tauri-apps/api
event module — window.__TAURI_INTERNALS__ doesn't exist in jsdom.

Plain 'vitest run' still passed (the rejection was async, after tests
finished), so the build job was green but coverage exited 1 on the
unhandled error.

Fix: add the same vi.mock that Popover.test.tsx already uses to
TimerCard.test.tsx, EditEntryDialog.test.tsx, and EntryRow.test.tsx
(EntryRow mounts EditEntryDialog when the row is clicked).
…fresh

Two Codex P2 flags on PR #30:

1) projects:changed reached TimerCard / Popover / EditEntryDialog but not
   EntryList (project + task labels on each row) or CalendarSection
   (project pills on calendar events). Result: after Solidtime-side
   rename or deletion the picker updates but mounted lists kept showing
   stale labels until remount. Both now subscribe + refetch.

2) Sync now (and refresh_projects) only emitted the frontend event;
   they didn't replay the Spotlight slices. pull_worker.rs does it on
   its 5-min tick, so deleted projects/tasks lingered in Spotlight
   results until a successful pull tick. New crate-private helper
   commands::sync::replace_spotlight_slices() mirrors pull_worker's
   notify_indexer(ProjectsReplaced) + notify_indexer(TasksReplaced)
   block. Called from both refresh paths.

EntryList.test.tsx gains the @tauri-apps/api/event mock alongside the
existing ones added in 5396157 — same root cause: jsdom's window has
no __TAURI_INTERNALS__ so any listen() call rejects.
@mariomeyer mariomeyer force-pushed the fix/refresh-prunes-deleted-reference-data branch from c06287e to acc879f Compare June 17, 2026 15:28
@mariomeyer mariomeyer merged commit 9188521 into main Jun 17, 2026
2 checks passed
@mariomeyer

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 0.5.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant