fix(viewer): hand off transcript ID only, fetch row from SQLite on mount

HistoryPage previously serialised the full TranscriptDto — text, segments,
manual + LLM tags, audio path — into `localStorage["kon_viewer_item"]` so
the viewer window could pick it up on mount. On a multi-hour transcript
that's MB-scale of user voice content sitting in storage that any
same-origin script in any open Kon window can read.

Hand off only `{ id }` (and a timestamp on re-saves). The viewer fetches
the canonical row from SQLite via the existing `get_transcript` Tauri
command and hydrates via the now-exported `mapTranscriptRow`. Cross-window
sync via the `storage` event still works — the receiving window re-fetches
on event instead of trusting the payload.

- HistoryPage `openViewer` + `openEditor`: write `{ id }` only.
- viewer `onMount` + `handleStorageChange`: route through new
  `loadFromHandoff` which calls `invoke("get_transcript", { id })`.
- viewer `saveItemToHistory`: re-stamp localStorage with `{ id, stamp }`
  to retrigger the storage event in sibling windows without leaking
  content.
- `mapTranscriptRow` exported from page.svelte.ts for the viewer's use.

Backward-compatible at the parse layer: the `{ id }` shape extracts cleanly
from a stale full-DTO payload (TranscriptEntry already carries `id` at top
level), so a session that survives the upgrade picks up the new path on
next handoff without manual cleanup.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
Claude
2026-04-25 08:47:12 +00:00
parent 3d568148b8
commit ab3bb9370c
3 changed files with 46 additions and 14 deletions

View File

@@ -335,12 +335,18 @@
}
async function openViewer(item) {
// Hand off the transcript ID only — the viewer window fetches the full
// row from SQLite via `get_transcript`. Previously the entire DTO
// (text + segments + tags) was stuffed into localStorage, putting
// potentially MB-scale PII in a place readable by any same-origin
// script.
const handoff = JSON.stringify({ id: String(item.id) });
try {
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
localStorage.setItem("kon_viewer_item", handoff);
localStorage.setItem("kon_viewer_mode", "view");
await invoke("open_viewer_window");
} catch {
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
localStorage.setItem("kon_viewer_item", handoff);
localStorage.setItem("kon_viewer_mode", "view");
window.open("/viewer", "_blank", "width=600,height=700");
}
@@ -499,12 +505,15 @@
});
async function openEditor(item) {
// Hand off the transcript ID only; the viewer hydrates from SQLite
// (see openViewer above for the rationale).
const handoff = JSON.stringify({ id: String(item.id) });
try {
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
localStorage.setItem("kon_viewer_item", handoff);
localStorage.setItem("kon_viewer_mode", "edit");
await invoke("open_viewer_window");
} catch {
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
localStorage.setItem("kon_viewer_item", handoff);
localStorage.setItem("kon_viewer_mode", "edit");
window.open("/viewer", "_blank", "width=600,height=700");
}

View File

@@ -140,7 +140,7 @@ export function saveProfiles() {
export const history = $state<TranscriptEntry[]>([]);
function mapTranscriptRow(row: TranscriptDto): TranscriptEntry {
export function mapTranscriptRow(row: TranscriptDto): TranscriptEntry {
const text = row.text ?? "";
const rawTags = row.manualTags ?? "";
const manualTags = rawTags ? rawTags.split(",").filter(Boolean) : [];