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(
state: tauri::State<'_, AppState>,
request: CreateTaskRequest,
) -> Result<(), String> {
) -> Result<TaskDto, String> {
db_insert_task(
&state.db,
&request.id,
@@ -76,7 +76,15 @@ pub async fn create_task_cmd(
request.source_transcript_id.as_deref(),
)
.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]

View File

@@ -221,65 +221,85 @@ export function deleteFromHistory(index) {
.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() {
try {
const raw = localStorage.getItem(TASKS_KEY);
if (raw) return JSON.parse(raw);
} catch {}
return [];
}
export const tasks = $state(loadTasks());
// On boot in the desktop runtime, load canonical tasks from SQLite.
// localStorage remains the warm cache and fallback.
if (typeof window !== "undefined" && hasTauriRuntime()) {
invoke("list_tasks_cmd")
.then((sqliteTasks) => {
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: "",
/** Shape a TaskDto row from the tasks commands into the UI entry shape
* that TasksPage / WipTaskList / TaskSidebar expect. Mirrors
* mapTranscriptRow's placement and field-defaulting pattern. */
function mapTaskRow(row) {
return {
id: row.id,
text: row.text ?? "",
bucket: row.bucket ?? "inbox",
listId: row.listId ?? null,
effort: row.effort ?? "",
done: !!row.done,
doneAt: row.doneAt ?? null,
createdAt: row.createdAt ?? new Date().toISOString(),
sourceTranscriptId: row.sourceTranscriptId ?? null,
parentTaskId: row.parentTaskId ?? null,
};
tasks.unshift(newTask);
saveTasks();
broadcastTasks();
if (hasTauriRuntime()) {
invoke("create_task_cmd", {
}
async function loadTasks() {
if (!hasTauriRuntime()) {
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: {
id: newTask.id,
text: newTask.text,
bucket: newTask.bucket,
sourceTranscriptId: newTask.sourceTranscriptId,
id,
text: task.text,
bucket: task.bucket || "inbox",
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);
if (idx >= 0) {
Object.assign(tasks[idx], updates);
saveTasks();
broadcastTasks();
}
}
export function deleteTask(id) {
const idx = tasks.findIndex((t) => t.id === id);
if (idx >= 0) {
tasks.splice(idx, 1);
saveTasks();
broadcastTasks();
if (hasTauriRuntime()) {
invoke("delete_task_cmd", { id }).catch(() => {});
export async function deleteTask(id) {
if (!hasTauriRuntime()) return;
try {
await invoke("delete_task_cmd", { id });
const idx = tasks.findIndex((t) => t.id === id);
if (idx >= 0) {
tasks.splice(idx, 1);
broadcastTasks();
}
} catch (err) {
toasts.error("Couldn't delete task", err?.message ?? String(err));
}
}
export function completeTask(id) {
updateTask(id, { done: true, doneAt: new Date().toISOString() });
if (hasTauriRuntime()) {
invoke("complete_task_cmd", { id }).catch(() => {});
export async function completeTask(id) {
if (!hasTauriRuntime()) return;
try {
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) {
updateTask(id, { done: false, doneAt: null });
if (hasTauriRuntime()) {
invoke("uncomplete_task_cmd", { id }).catch(() => {});
export async function uncompleteTask(id) {
if (!hasTauriRuntime()) return;
try {
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 ----
const taskChannel = typeof BroadcastChannel !== "undefined"
? new BroadcastChannel("kon_tasks")
? new BroadcastChannel("kon_task_sync")
: null;
if (taskChannel) {