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 {}
}
// ---- 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() {
try {
const raw = localStorage.getItem(HISTORY_KEY);
if (raw) return JSON.parse(raw);
} catch {}
return [];
/** 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),
};
}
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;
async function loadHistory() {
if (!hasTauriRuntime()) {
history.length = 0;
return;
}
try {
const next = JSON.parse(e.newValue);
history.splice(0, history.length, ...next);
} catch {}
});
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 function saveHistory() {
try {
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
} catch {}
// Hydrate history from SQLite on boot. Mirrors the tasks hydration pattern
// further down this file.
if (typeof window !== "undefined" && hasTauriRuntime()) {
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
* 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;