fix: harden frontend — toggle race guard, fresh debounce snapshot, error surfacing

Add toggling guard to TaskItem preventing double-toggle from rapid clicks
or swipes. Fix debounced save in TaskDetailView to read task at timeout
time instead of capturing a stale snapshot at call time. Remove unused
_unlistenFs variable. Surface reorder_task and watch_workspace errors
instead of silently swallowing them.
This commit is contained in:
Tristan Michael 2026-04-02 08:58:54 -07:00
parent 3c11539f02
commit 7fde1a09f9
3 changed files with 10 additions and 10 deletions

View file

@ -34,9 +34,8 @@
function debouncedSave(fields: Partial<Task>) {
clearTimeout(saveTimer);
const snapshot = { ...task };
saveTimer = setTimeout(() => {
app.updateTask({ ...snapshot, ...fields, updated_at: new Date().toISOString() });
app.updateTask({ ...task, ...fields, updated_at: new Date().toISOString() });
}, 400);
}

View file

@ -19,6 +19,7 @@
let isCompleted = $derived(task.status === "completed");
let justChecked = $state(false);
let toggling = $state(false);
$effect(() => {
const _ = task.status;
@ -35,6 +36,8 @@
async function handleToggle(e: MouseEvent) {
e.stopPropagation();
if (toggling) return;
toggling = true;
justChecked = true;
await new Promise((r) => setTimeout(r, 300));
transitioning = true;
@ -42,6 +45,7 @@
await new Promise((r) => setTimeout(r, 200));
justChecked = false;
await app.toggleTask(task.id);
toggling = false;
}
function handleTouchStart(e: TouchEvent) {
@ -57,14 +61,15 @@
}
function handleTouchEnd() {
if (Math.abs(swipeX) > 100) {
if (Math.abs(swipeX) > 100 && !toggling) {
swipeX = 0;
swiping = false;
toggling = true;
justChecked = true;
setTimeout(() => {
transitioning = true;
animateInIds.add(task.id);
setTimeout(() => { justChecked = false; app.toggleTask(task.id); }, 200);
setTimeout(() => { justChecked = false; app.toggleTask(task.id).finally(() => { toggling = false; }); }, 200);
}, 300);
return;
}

View file

@ -9,12 +9,8 @@ import type {
} from "../types";
// Listen for file system changes from the backend watcher.
// Store the unlisten function so it can be cleaned up if needed.
let _unlistenFs: (() => void) | null = null;
listen("fs-changed", () => {
loadLists();
}).then((unlisten) => {
_unlistenFs = unlisten;
});
// ── Reactive state ───────────────────────────────────────────────────
@ -98,7 +94,7 @@ async function switchWorkspace(name: string) {
activeListId = null;
await loadLists();
const ws = config?.workspaces[name];
if (ws) invoke("watch_workspace", { path: ws.path }).catch(() => {});
if (ws) invoke("watch_workspace", { path: ws.path }).catch((e) => console.warn("File watcher failed:", e));
error = null;
} catch (e) {
error = String(e);
@ -200,7 +196,7 @@ async function toggleTask(taskId: string) {
// Move to top of list locally, then persist order in background
if (updated.status === "backlog") {
tasks = [updated, ...tasks.filter((t) => t.id !== taskId)];
invoke("reorder_task", { listId: activeListId, taskId, newPosition: 0 }).catch(() => {});
invoke("reorder_task", { listId: activeListId, taskId, newPosition: 0 }).catch((e) => { error = String(e); });
} else {
tasks = tasks.map((t) => (t.id === taskId ? updated : t));
}