fix: improve error handling across Rust and TypeScript

- Log swallowed errors in file watcher and window operations (lib.rs, watcher.rs)
- Propagate recovery errors from bridge::recover_pending to SyncResult.errors
  so the frontend can display them instead of silently dropping failures
- Fix useTauriEvent/useTauriEvents race condition where cleanup fires before
  async listen() resolves, leaking the listener (cancelled flag pattern)
- Guard computeStaleness against invalid date strings (NaN -> 'normal'
  instead of incorrectly returning 'urgent')
- Strengthen isMcError type guard to check field types, not just presence
- Log warning when data directory resolution falls back to '.' (state.rs, bridge.rs)
- Add test for computeStaleness with invalid date inputs
This commit is contained in:
teernisse
2026-02-26 10:13:17 -05:00
parent 29b44f1b4c
commit 23a4e6bf19
7 changed files with 556 additions and 13 deletions

182
src/hooks/useActions.ts Normal file
View File

@@ -0,0 +1,182 @@
/**
* useActions -- actions for the focus workflow.
*
* Handles start, defer, skip, complete actions with:
* - Decision logging to backend
* - State updates (snooze times, skipped flags)
* - URL opening for start action
* - Queue advancement via focus store
*/
import { useCallback } from "react";
import { open } from "@tauri-apps/plugin-shell";
import { invoke } from "@tauri-apps/api/core";
import { useFocusStore } from "@/stores/focus-store";
import type { DeferDuration, FocusAction } from "@/lib/types";
/** Minimal item shape needed for actions */
export interface ActionItem {
id: string;
title: string;
url?: string;
}
/** Decision entry sent to backend */
interface DecisionEntry {
action: string;
bead_id: string;
reason?: string | null;
}
/**
* Calculate the snooze-until timestamp for a defer action.
* All times are calculated in UTC for consistency.
*/
function calculateSnoozeTime(duration: DeferDuration): string {
const now = new Date();
switch (duration) {
case "1h":
return new Date(now.getTime() + 60 * 60 * 1000).toISOString();
case "3h":
return new Date(now.getTime() + 3 * 60 * 60 * 1000).toISOString();
case "tomorrow": {
// 9am UTC next day
const tomorrow = new Date(now);
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
tomorrow.setUTCHours(9, 0, 0, 0);
return tomorrow.toISOString();
}
case "next_week": {
// 9am UTC next Monday
const nextWeek = new Date(now);
const daysUntilMonday = (8 - nextWeek.getUTCDay()) % 7 || 7;
nextWeek.setUTCDate(nextWeek.getUTCDate() + daysUntilMonday);
nextWeek.setUTCHours(9, 0, 0, 0);
return nextWeek.toISOString();
}
}
}
/**
* Log a decision to the backend.
*/
async function logDecision(entry: DecisionEntry): Promise<void> {
await invoke("log_decision", { entry });
}
export interface UseActionsReturn {
/** Start working on an item (opens URL if present) */
start: (item: ActionItem) => Promise<void>;
/** Defer an item for later */
defer: (
item: ActionItem,
duration: DeferDuration,
reason: string | null
) => Promise<void>;
/** Skip an item for today */
skip: (item: ActionItem, reason: string | null) => Promise<void>;
/** Mark an item as complete */
complete: (item: ActionItem, reason: string | null) => Promise<void>;
}
/**
* Hook providing focus workflow actions.
*
* Each action:
* 1. Performs the relevant side effect (open URL, update state)
* 2. Logs the decision to the backend
* 3. Advances to the next item in the queue
*/
export function useActions(): UseActionsReturn {
const { act } = useFocusStore();
const start = useCallback(async (item: ActionItem): Promise<void> => {
// Open URL in browser if provided
if (item.url) {
await open(item.url);
}
// Log the decision
await logDecision({
action: "start",
bead_id: item.id,
});
}, []);
const defer = useCallback(
async (
item: ActionItem,
duration: DeferDuration,
reason: string | null
): Promise<void> => {
const snoozedUntil = calculateSnoozeTime(duration);
// Update item with snooze time
await invoke("update_item", {
id: item.id,
updates: {
snoozed_until: snoozedUntil,
},
});
// Log the decision
await logDecision({
action: "defer",
bead_id: item.id,
reason,
});
// Convert duration to FocusAction format and advance queue
const actionName: FocusAction = `defer_${duration}` as FocusAction;
act(actionName, reason ?? undefined);
},
[act]
);
const skip = useCallback(
async (item: ActionItem, reason: string | null): Promise<void> => {
// Mark item as skipped for today
await invoke("update_item", {
id: item.id,
updates: {
skipped_today: true,
},
});
// Log the decision
await logDecision({
action: "skip",
bead_id: item.id,
reason,
});
// Advance queue
act("skip", reason ?? undefined);
},
[act]
);
const complete = useCallback(
async (item: ActionItem, reason: string | null): Promise<void> => {
// Close the bead via backend
await invoke("close_bead", {
bead_id: item.id,
reason,
});
// Log the decision
await logDecision({
action: "complete",
bead_id: item.id,
reason,
});
// Advance queue
act("skip", reason ?? undefined); // Uses skip action to advance
},
[act]
);
return { start, defer, skip, complete };
}