refactor(state): drop kon_tasks localStorage cache — Tauri-first, UI-on-success
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user