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:
182
src/hooks/useActions.ts
Normal file
182
src/hooks/useActions.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user