refactor(state): drop kon_history localStorage cache — SQLite canonical

This commit is contained in:
2026-04-19 15:59:04 +01:00
parent 1a849f9e7f
commit 5e3bc369de

View File

@@ -75,55 +75,79 @@ export function saveProfiles() {
} catch {} } 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 // The canonical store is the SQLite transcripts table reachable via the
// transcripts table reachable via the `add_transcript`, `list_transcripts`, // `add_transcript`, `list_transcripts`, `update_transcript`,
// `update_transcript`, `delete_transcript`, `search_transcripts` Tauri // `delete_transcript`, `search_transcripts` Tauri commands. The history
// commands. localStorage continues to back the in-memory `history` array // array is seeded empty on module init and hydrated from SQLite on boot
// for UI snappiness and offline browser-preview support, but the source // in the desktop runtime. Browser preview (no Tauri) shows an empty list.
// of truth is SQLite. Browser preview falls back to localStorage only. //
// 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 { invoke } from "@tauri-apps/api/core";
import { hasTauriRuntime } from "$lib/utils/runtime.js"; 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
try { * that HistoryPage expects. UI-only fields that were never persisted to
const raw = localStorage.getItem(HISTORY_KEY); * SQLite (segments, starred, manualTags, language, template) are absent. */
if (raw) return JSON.parse(raw); function mapTranscriptRow(row) {
} catch {} const text = row.text ?? "";
return []; 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),
};
} }
export const history = $state(loadHistory()); async function loadHistory() {
if (!hasTauriRuntime()) {
// Keep the in-memory history in sync with edits made by sibling windows history.length = 0;
// (e.g. the viewer saving a cleaned-up transcript). Storage events fire return;
// 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 { try {
const next = JSON.parse(e.newValue); const rows = await invoke("list_transcripts", { limit: 500, offset: 0 });
history.splice(0, history.length, ...next); const mapped = Array.isArray(rows) ? rows.map(mapTranscriptRow) : [];
} catch {} 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 function saveHistory() { // Hydrate history from SQLite on boot. Mirrors the tasks hydration pattern
try { // further down this file.
localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); if (typeof window !== "undefined" && hasTauriRuntime()) {
} catch {} loadHistory().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 * Add a transcript to history. Writes to SQLite (canonical) and updates
* localStorage (cache). The SQLite write is best-effort: if it fails, * the in-memory `history` array. SQLite is best-effort: if it fails we
* we still update the in-memory + localStorage copy so the UI stays * keep the in-memory copy so the UI stays responsive for the session,
* responsive. The next session boot reconciles by reading SQLite first. * but the entry will not survive a restart.
* *
* `entry` shape: * `entry` shape:
* { id, text, source?, title?, audioPath?, duration?, engine?, modelId?, * { id, text, source?, title?, audioPath?, duration?, engine?, modelId?,
@@ -132,10 +156,9 @@ export function saveHistory() {
*/ */
export async function addToHistory(entry) { export async function addToHistory(entry) {
history.unshift(entry); history.unshift(entry);
if (history.length > 100) history.length = 100; if (history.length > 500) history.length = 500;
saveHistory();
if (!hasTauriRuntime()) return; // Browser preview: localStorage only. if (!hasTauriRuntime()) return; // Browser preview: in-memory only.
try { try {
await invoke("add_transcript", { await invoke("add_transcript", {
@@ -158,22 +181,20 @@ export async function addToHistory(entry) {
}, },
}); });
} catch (err) { } 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 * Update text and/or title of an existing transcript. Persists to SQLite
* via update_transcript; updates the in-memory + localStorage cache. * via update_transcript; updates the in-memory entry. Closes the historic
* Closes the historic "rename never persists" bug from * "rename never persists" bug from architecture-review.md §13.
* architecture-review.md §13.
*/ */
export async function renameHistoryEntry(id, updates) { export async function renameHistoryEntry(id, updates) {
const idx = history.findIndex(h => String(h.id) === String(id)); const idx = history.findIndex(h => String(h.id) === String(id));
if (idx >= 0) { if (idx >= 0) {
if (updates.title !== undefined) history[idx].title = updates.title; if (updates.title !== undefined) history[idx].title = updates.title;
if (updates.text !== undefined) history[idx].text = updates.text; if (updates.text !== undefined) history[idx].text = updates.text;
saveHistory();
} }
if (!hasTauriRuntime()) return; if (!hasTauriRuntime()) return;
@@ -193,7 +214,6 @@ export async function renameHistoryEntry(id, updates) {
export function deleteFromHistory(index) { export function deleteFromHistory(index) {
const entry = history[index]; const entry = history[index];
history.splice(index, 1); history.splice(index, 1);
saveHistory();
if (!hasTauriRuntime() || !entry?.id) return; if (!hasTauriRuntime() || !entry?.id) return;