refactor(state): drop kon_tasks localStorage cache — Tauri-first, UI-on-success

This commit is contained in:
2026-04-19 16:14:49 +01:00
parent 6113e6d784
commit db5c739f22
2 changed files with 109 additions and 74 deletions

View File

@@ -67,7 +67,7 @@ pub struct CreateTaskRequest {
pub async fn create_task_cmd( pub async fn create_task_cmd(
state: tauri::State<'_, AppState>, state: tauri::State<'_, AppState>,
request: CreateTaskRequest, request: CreateTaskRequest,
) -> Result<(), String> { ) -> Result<TaskDto, String> {
db_insert_task( db_insert_task(
&state.db, &state.db,
&request.id, &request.id,
@@ -76,7 +76,15 @@ pub async fn create_task_cmd(
request.source_transcript_id.as_deref(), request.source_transcript_id.as_deref(),
) )
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())?;
// Fetch the freshly-inserted row so the frontend can stop doing
// client-side object construction. Mirrors list_tasks_cmd's shape.
db_get_task(&state.db, &request.id)
.await
.map_err(|e| e.to_string())?
.map(TaskDto::from)
.ok_or_else(|| format!("Task {} not found after insert", request.id))
} }
#[tauri::command] #[tauri::command]

View File

@@ -221,65 +221,85 @@ export function deleteFromHistory(index) {
.catch(err => console.warn("deleteFromHistory: SQLite delete failed", err)); .catch(err => console.warn("deleteFromHistory: SQLite delete failed", err));
} }
// ---- Tasks (persisted to localStorage) ---- // ---- Tasks (SQLite via Tauri = canonical; no localStorage cache) ----
//
// The canonical store is the SQLite tasks table reachable via the
// `create_task_cmd`, `list_tasks_cmd`, `delete_task_cmd`,
// `complete_task_cmd`, `uncomplete_task_cmd` Tauri commands. The tasks
// array is seeded empty on module init and hydrated from SQLite on boot
// in the desktop runtime. Browser preview (no Tauri) shows an empty list.
//
// The previous localStorage tasks cache was removed to kill the cold-
// start flicker and the dual-write drift between localStorage and SQLite.
const TASKS_KEY = "kon_tasks"; export const tasks = $state([]);
function loadTasks() { /** Shape a TaskDto row from the tasks commands into the UI entry shape
try { * that TasksPage / WipTaskList / TaskSidebar expect. Mirrors
const raw = localStorage.getItem(TASKS_KEY); * mapTranscriptRow's placement and field-defaulting pattern. */
if (raw) return JSON.parse(raw); function mapTaskRow(row) {
} catch {} return {
return []; id: row.id,
} text: row.text ?? "",
bucket: row.bucket ?? "inbox",
export const tasks = $state(loadTasks()); listId: row.listId ?? null,
effort: row.effort ?? "",
// On boot in the desktop runtime, load canonical tasks from SQLite. done: !!row.done,
// localStorage remains the warm cache and fallback. doneAt: row.doneAt ?? null,
if (typeof window !== "undefined" && hasTauriRuntime()) { createdAt: row.createdAt ?? new Date().toISOString(),
invoke("list_tasks_cmd") sourceTranscriptId: row.sourceTranscriptId ?? null,
.then((sqliteTasks) => { parentTaskId: row.parentTaskId ?? null,
tasks.length = 0;
tasks.push(...sqliteTasks);
saveTasks();
})
.catch(() => {});
}
export function saveTasks() {
try {
localStorage.setItem(TASKS_KEY, JSON.stringify(tasks));
} catch {}
}
export function addTask(task) {
const newTask = {
id: crypto.randomUUID(),
text: task.text,
bucket: task.bucket || "inbox",
listId: task.listId || null,
context: task.context || "",
effort: task.effort || "",
done: false,
doneAt: null,
createdAt: new Date().toISOString(),
sourceTranscriptId: task.sourceTranscriptId || null,
parentTaskId: task.parentTaskId || null,
notes: "",
}; };
tasks.unshift(newTask); }
saveTasks();
broadcastTasks(); async function loadTasks() {
if (hasTauriRuntime()) { if (!hasTauriRuntime()) {
invoke("create_task_cmd", { tasks.length = 0;
return;
}
try {
const rows = await invoke("list_tasks_cmd");
const mapped = Array.isArray(rows) ? rows.map(mapTaskRow) : [];
tasks.splice(0, tasks.length, ...mapped);
} catch (err) {
console.error("loadTasks failed", err);
tasks.length = 0;
toasts.error("Failed to load tasks", err?.message ?? String(err));
}
}
// Hydrate tasks from SQLite on boot. Mirrors the history hydration pattern
// earlier in this file.
if (typeof window !== "undefined" && hasTauriRuntime()) {
loadTasks().catch(() => {});
}
// `saveTasks` is retained as a no-op for callers in the Task Lists section
// further down (removeProfileTaskList, deleteTaskList) that mutate
// `t.listId` to null when a list is deleted and previously flushed via
// localStorage. SQLite writes happen via the specific `create_task_cmd`
// / `delete_task_cmd` / `complete_task_cmd` / `uncomplete_task_cmd`
// commands in the functions below; the listId-clearing mutation is
// currently session-scoped because listId is not round-tripped through
// those commands. See report note.
export function saveTasks() {}
export async function addTask(task) {
if (!hasTauriRuntime()) return;
const id = crypto.randomUUID();
try {
const row = await invoke("create_task_cmd", {
request: { request: {
id: newTask.id, id,
text: newTask.text, text: task.text,
bucket: newTask.bucket, bucket: task.bucket || "inbox",
sourceTranscriptId: newTask.sourceTranscriptId, sourceTranscriptId: task.sourceTranscriptId || null,
}, },
}).catch(() => {}); });
tasks.unshift(mapTaskRow(row));
broadcastTasks();
} catch (err) {
toasts.error("Couldn't add task", err?.message ?? String(err));
} }
} }
@@ -287,41 +307,48 @@ export function updateTask(id, updates) {
const idx = tasks.findIndex((t) => t.id === id); const idx = tasks.findIndex((t) => t.id === id);
if (idx >= 0) { if (idx >= 0) {
Object.assign(tasks[idx], updates); Object.assign(tasks[idx], updates);
saveTasks();
broadcastTasks(); broadcastTasks();
} }
} }
export function deleteTask(id) { export async function deleteTask(id) {
const idx = tasks.findIndex((t) => t.id === id); if (!hasTauriRuntime()) return;
if (idx >= 0) { try {
tasks.splice(idx, 1); await invoke("delete_task_cmd", { id });
saveTasks(); const idx = tasks.findIndex((t) => t.id === id);
broadcastTasks(); if (idx >= 0) {
if (hasTauriRuntime()) { tasks.splice(idx, 1);
invoke("delete_task_cmd", { id }).catch(() => {}); broadcastTasks();
} }
} catch (err) {
toasts.error("Couldn't delete task", err?.message ?? String(err));
} }
} }
export function completeTask(id) { export async function completeTask(id) {
updateTask(id, { done: true, doneAt: new Date().toISOString() }); if (!hasTauriRuntime()) return;
if (hasTauriRuntime()) { try {
invoke("complete_task_cmd", { id }).catch(() => {}); await invoke("complete_task_cmd", { id });
updateTask(id, { done: true, doneAt: new Date().toISOString() });
} catch (err) {
toasts.error("Couldn't complete task", err?.message ?? String(err));
} }
} }
export function uncompleteTask(id) { export async function uncompleteTask(id) {
updateTask(id, { done: false, doneAt: null }); if (!hasTauriRuntime()) return;
if (hasTauriRuntime()) { try {
invoke("uncomplete_task_cmd", { id }).catch(() => {}); await invoke("uncomplete_task_cmd", { id });
updateTask(id, { done: false, doneAt: null });
} catch (err) {
toasts.error("Couldn't uncomplete task", err?.message ?? String(err));
} }
} }
// ---- BroadcastChannel for multi-window task sync ---- // ---- BroadcastChannel for multi-window task sync ----
const taskChannel = typeof BroadcastChannel !== "undefined" const taskChannel = typeof BroadcastChannel !== "undefined"
? new BroadcastChannel("kon_tasks") ? new BroadcastChannel("kon_task_sync")
: null; : null;
if (taskChannel) { if (taskChannel) {