Sync prune: refresh_reference_data removes server-side deletions + projects:changed event#30
Conversation
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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); |
There was a problem hiding this comment.
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 👍 / 👎.
…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.
c06287e to
acc879f
Compare
|
🎉 This PR is included in version 0.5.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Two bugs the user hit after reorganizing projects in Solidtime:
Stale projects/tasks/clients/tags lingered locally.
refresh_reference_dataonly upserted — it never pruned. Solidtime-side deletions were invisible to stint.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 (setarchived = 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 (noarchivedcolumn on those tables). Entries with danglingtask_idshow no task name; acceptable.refresh_reference_datanow calls the prune helpers after each upsert.ON CONFLICT … SET archived = excluded.archivedun-archives it on the same tick. (Regression test.)Tauri layer (stint-app):
EVENT_PROJECTS_CHANGED = "projects:changed"constant.commands::sync::sync_nowemits it after a successfulrefresh_reference_data.commands::projects::refresh_projectsemits it too (and gains anAppHandle<R>parameter).projects_commands.rstests passhandle.clone()through.UI (ui/src):
Popover.tsx,TimerCard.tsx,EditEntryDialog.tsxlisten forprojects:changedand callrefetch()on their projects (and tasks) resources.Test plan
sync_refresh.rs)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