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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) : [];
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||
import type { TranscriptEntry, ViewerSegment } from "$lib/types/app";
|
||||
import type { TranscriptDto, TranscriptEntry, ViewerSegment } from "$lib/types/app";
|
||||
import SpeakerButton from "$lib/components/SpeakerButton.svelte";
|
||||
import { formatTime, formatDuration } from "$lib/utils/time.js";
|
||||
import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js";
|
||||
import { errorMessage } from "$lib/utils/errors.js";
|
||||
import { parseStoredJson } from "$lib/utils/storage.js";
|
||||
import { saveTranscriptMeta } from "$lib/stores/page.svelte.js";
|
||||
import { mapTranscriptRow, saveTranscriptMeta } from "$lib/stores/page.svelte.js";
|
||||
import { toasts } from "$lib/stores/toasts.svelte.ts";
|
||||
import { DEFAULT_PROFILE_ID } from "$lib/stores/profiles.svelte.ts";
|
||||
|
||||
@@ -71,8 +71,28 @@
|
||||
buildAudio(nextItem?.audioPath ?? null);
|
||||
}
|
||||
|
||||
// Hydrate the viewer from a transcript ID handed off via localStorage.
|
||||
// The full row is fetched from SQLite via `get_transcript` so transcript
|
||||
// text never lives in localStorage where any same-origin script could
|
||||
// read it.
|
||||
async function loadFromHandoff(raw: string | null) {
|
||||
const handoff = parseStoredJson<{ id?: unknown }>(raw);
|
||||
const id = handoff?.id != null ? String(handoff.id) : "";
|
||||
if (!id) {
|
||||
loadViewerItem(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dto = await invoke<TranscriptDto | null>("get_transcript", { id });
|
||||
loadViewerItem(dto ? mapTranscriptRow(dto) : null);
|
||||
} catch (err) {
|
||||
console.warn("viewer get_transcript failed", errorMessage(err));
|
||||
loadViewerItem(null);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadViewerItem(parseStoredJson<TranscriptEntry>(localStorage.getItem("kon_viewer_item")));
|
||||
loadFromHandoff(localStorage.getItem("kon_viewer_item"));
|
||||
const mode = localStorage.getItem("kon_viewer_mode");
|
||||
if (mode === "edit" || mode === "view") viewerMode = mode;
|
||||
|
||||
@@ -120,7 +140,7 @@
|
||||
|
||||
function handleStorageChange(e: StorageEvent) {
|
||||
if (e.key === "kon_viewer_item" && e.newValue) {
|
||||
loadViewerItem(parseStoredJson<TranscriptEntry>(e.newValue));
|
||||
void loadFromHandoff(e.newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,14 +309,17 @@
|
||||
// store's `saveTranscriptMeta` helper — callers in this file wire those
|
||||
// up alongside the plain-text update below.
|
||||
//
|
||||
// The cross-window `kon_viewer_item` localStorage payload stays in sync so
|
||||
// a second viewer window hydrating from it reflects the latest edit.
|
||||
// We re-stamp the localStorage handoff with the same ID so a sibling
|
||||
// viewer window receives a `storage` event and re-fetches from SQLite.
|
||||
// The handoff intentionally contains no transcript content — see
|
||||
// `loadFromHandoff` for the rationale.
|
||||
function saveItemToHistory() {
|
||||
if (!item) return;
|
||||
// Update the cross-window payload only. The old localStorage history
|
||||
// cache was removed as part of the SQLite-canonical refactor.
|
||||
try {
|
||||
localStorage.setItem("kon_viewer_item", JSON.stringify(item));
|
||||
localStorage.setItem(
|
||||
"kon_viewer_item",
|
||||
JSON.stringify({ id: String(item.id), stamp: Date.now() }),
|
||||
);
|
||||
} catch {}
|
||||
// Best-effort SQLite persistence. Silently ignored in browser preview
|
||||
// (no Tauri runtime) — matches the main-window store's behaviour.
|
||||
|
||||
Reference in New Issue
Block a user