diff --git a/src/lib/stores/page.svelte.js b/src/lib/stores/page.svelte.js index c1c02e9..34a1bb4 100644 --- a/src/lib/stores/page.svelte.js +++ b/src/lib/stores/page.svelte.js @@ -75,55 +75,79 @@ export function saveProfiles() { } catch {} } -// ---- History (dual-write: SQLite via Tauri = canonical, localStorage = read-cache) ---- +// ---- History (SQLite via Tauri = canonical; no localStorage cache) ---- // -// As of Day 4 of the upgrade plan, the canonical store is the SQLite -// transcripts table reachable via the `add_transcript`, `list_transcripts`, -// `update_transcript`, `delete_transcript`, `search_transcripts` Tauri -// commands. localStorage continues to back the in-memory `history` array -// for UI snappiness and offline browser-preview support, but the source -// of truth is SQLite. Browser preview falls back to localStorage only. +// The canonical store is the SQLite transcripts table reachable via the +// `add_transcript`, `list_transcripts`, `update_transcript`, +// `delete_transcript`, `search_transcripts` Tauri commands. The history +// 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 `kon_history` localStorage read-cache was removed to kill +// the cold-start flicker where the UI painted stale localStorage data and +// then overwrote it from SQLite a tick later. import { invoke } from "@tauri-apps/api/core"; import { hasTauriRuntime } from "$lib/utils/runtime.js"; +import { toasts } from "$lib/stores/toasts.svelte.js"; -const HISTORY_KEY = "kon_history"; +export const history = $state([]); -function loadHistory() { +/** Shape a TranscriptDto row from `list_transcripts` into the UI entry shape + * that HistoryPage expects. UI-only fields that were never persisted to + * SQLite (segments, starred, manualTags, language, template) are absent. */ +function mapTranscriptRow(row) { + const text = row.text ?? ""; + return { + id: row.id, + text, + source: row.source ?? "", + title: row.title ?? "", + audioPath: row.audioPath ?? null, + duration: Number(row.duration ?? 0), + engine: row.engine ?? null, + modelId: row.modelId ?? null, + createdAt: row.createdAt ?? null, + date: row.createdAt ? new Date(row.createdAt).toLocaleString("en-GB") : "", + preview: text.slice(0, 120), + }; +} + +async function loadHistory() { + if (!hasTauriRuntime()) { + history.length = 0; + return; + } try { - const raw = localStorage.getItem(HISTORY_KEY); - if (raw) return JSON.parse(raw); - } catch {} - return []; + const rows = await invoke("list_transcripts", { limit: 500, offset: 0 }); + const mapped = Array.isArray(rows) ? rows.map(mapTranscriptRow) : []; + history.splice(0, history.length, ...mapped); + } catch (err) { + console.error("loadHistory failed", err); + history.length = 0; + toasts.error("Failed to load history", err?.message ?? String(err)); + } } -export const history = $state(loadHistory()); - -// Keep the in-memory history in sync with edits made by sibling windows -// (e.g. the viewer saving a cleaned-up transcript). Storage events fire -// only on *other* windows than the writer, so we won't re-enter our own -// writes. -if (typeof window !== "undefined") { - window.addEventListener("storage", (e) => { - if (e.key !== HISTORY_KEY || !e.newValue) return; - try { - const next = JSON.parse(e.newValue); - history.splice(0, history.length, ...next); - } catch {} - }); +// Hydrate history from SQLite on boot. Mirrors the tasks hydration pattern +// further down this file. +if (typeof window !== "undefined" && hasTauriRuntime()) { + loadHistory().catch(() => {}); } -export function saveHistory() { - try { - localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); - } catch {} -} +// `saveHistory` is retained as a no-op for callers in HistoryPage that use +// it as a mutation hook (tag add/remove, clear-all). SQLite writes happen +// via the specific `add_transcript` / `update_transcript` / +// `delete_transcript` commands in the functions below; ad-hoc UI-only +// mutations (e.g. manualTags) are session-scoped because they are not +// backed by a SQLite column. +export function saveHistory() {} /** - * Add a transcript to history. Dual-writes to SQLite (canonical) and - * localStorage (cache). The SQLite write is best-effort: if it fails, - * we still update the in-memory + localStorage copy so the UI stays - * responsive. The next session boot reconciles by reading SQLite first. + * Add a transcript to history. Writes to SQLite (canonical) and updates + * the in-memory `history` array. SQLite is best-effort: if it fails we + * keep the in-memory copy so the UI stays responsive for the session, + * but the entry will not survive a restart. * * `entry` shape: * { id, text, source?, title?, audioPath?, duration?, engine?, modelId?, @@ -132,10 +156,9 @@ export function saveHistory() { */ export async function addToHistory(entry) { history.unshift(entry); - if (history.length > 100) history.length = 100; - saveHistory(); + if (history.length > 500) history.length = 500; - if (!hasTauriRuntime()) return; // Browser preview: localStorage only. + if (!hasTauriRuntime()) return; // Browser preview: in-memory only. try { await invoke("add_transcript", { @@ -158,22 +181,20 @@ export async function addToHistory(entry) { }, }); } catch (err) { - console.warn("addToHistory: SQLite dual-write failed, kept in localStorage", err); + console.warn("addToHistory: SQLite write failed, entry will not persist past restart", err); } } /** * Update text and/or title of an existing transcript. Persists to SQLite - * via update_transcript; updates the in-memory + localStorage cache. - * Closes the historic "rename never persists" bug from - * architecture-review.md §13. + * via update_transcript; updates the in-memory entry. Closes the historic + * "rename never persists" bug from architecture-review.md §13. */ export async function renameHistoryEntry(id, updates) { const idx = history.findIndex(h => String(h.id) === String(id)); if (idx >= 0) { if (updates.title !== undefined) history[idx].title = updates.title; if (updates.text !== undefined) history[idx].text = updates.text; - saveHistory(); } if (!hasTauriRuntime()) return; @@ -193,7 +214,6 @@ export async function renameHistoryEntry(id, updates) { export function deleteFromHistory(index) { const entry = history[index]; history.splice(index, 1); - saveHistory(); if (!hasTauriRuntime() || !entry?.id) return;