ui: Day 4 frontend — dual-write history to SQLite + persist History rename

addToHistory, renameHistoryEntry, deleteFromHistory in
src/lib/stores/page.svelte.js now dual-write to:
- the in-memory `history` array (UI snappiness, unchanged)
- localStorage (offline / browser-preview fallback, unchanged)
- SQLite via the new Tauri commands from 1cce567 (the canonical store)

The SQLite write is best-effort: failure does not lose the in-memory
copy. Console-warns for diagnostic visibility. Browser-preview path
correctly skips the Tauri call.

HistoryPage rename flow (renameItem) now calls renameHistoryEntry, which
goes through update_transcript. Closes the long-standing TODO from
architecture-review.md §13: rename was UI-only, never persisted, lost
on reload.

On rename failure a warn-toast surfaces "your change is visible now but
did not save" so the user is not surprised on next launch.

NOT WIRED YET (deferred):
- HistoryPage.searchQuery still filters in-memory rather than calling
  search_transcripts. Fine for small histories; FTS5 infrastructure is
  in place to upgrade when needed.
- Reading initial history from SQLite on session boot. localStorage
  remains the cold-start source for now; SQLite catches up via dual-
  write. A backfill / one-time sync command can land later.
This commit is contained in:
2026-04-17 13:12:38 +01:00
parent 1cce5670af
commit 0e22ec591d
2 changed files with 385 additions and 133 deletions

View File

@@ -75,7 +75,17 @@ export function saveProfiles() {
} catch {}
}
// ---- History (persisted to localStorage) ----
// ---- History (dual-write: SQLite via Tauri = canonical, localStorage = read-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.
import { invoke } from "@tauri-apps/api/core";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
const HISTORY_KEY = "kon_history";
@@ -95,15 +105,86 @@ export function saveHistory() {
} catch {}
}
export function addToHistory(entry) {
/**
* 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.
*
* `entry` shape:
* { id, text, source?, title?, audioPath?, duration?, engine?, modelId?,
* inferenceMs?, sampleRate?, audioChannels?, formatMode?,
* removeFillers?, britishEnglish?, antiHallucination?, ...uiOnlyFields }
*/
export async function addToHistory(entry) {
history.unshift(entry);
if (history.length > 100) history.length = 100;
saveHistory();
if (!hasTauriRuntime()) return; // Browser preview: localStorage only.
try {
await invoke("add_transcript", {
transcript: {
id: String(entry.id),
text: entry.text ?? "",
source: entry.source ?? "microphone",
title: entry.title ?? null,
audioPath: entry.audioPath ?? null,
duration: Number(entry.duration ?? 0),
engine: entry.engine ?? null,
modelId: entry.modelId ?? null,
inferenceMs: entry.inferenceMs ?? null,
sampleRate: entry.sampleRate ?? null,
audioChannels: entry.audioChannels ?? null,
formatMode: entry.formatMode ?? null,
removeFillers: !!entry.removeFillers,
britishEnglish: !!entry.britishEnglish,
antiHallucination: !!entry.antiHallucination,
},
});
} catch (err) {
console.warn("addToHistory: SQLite dual-write failed, kept in localStorage", 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.
*/
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;
try {
await invoke("update_transcript", {
id: String(id),
text: updates.text ?? null,
title: updates.title ?? null,
});
} catch (err) {
console.warn("renameHistoryEntry: SQLite update failed", err);
throw err;
}
}
export function deleteFromHistory(index) {
const entry = history[index];
history.splice(index, 1);
saveHistory();
if (!hasTauriRuntime() || !entry?.id) return;
invoke("delete_transcript", { id: String(entry.id) })
.catch(err => console.warn("deleteFromHistory: SQLite delete failed", err));
}
// ---- Tasks (persisted to localStorage) ----