Recents rework: stable-ID storage, rename/delete, auto-add + autosave - #438
Merged
Conversation
Entries are now keyed by a generated ID (hyperaudio:doc:<id>) with the display name in meta.name, instead of the name BEING the key. Renaming becomes a one-field update, saving a name that already exists gets a " (2)" suffix instead of silently overwriting, and cached media keys off meta.mediaKey so a rename never re-keys a large IndexedDB blob. Legacy <name>.hyperaudio entries migrate in place the first time the list renders — same JSON, new key, name/mediaKey carried into meta (media stays under its old key). An entry that does not parse stays on its legacy key: still listed, still deletable, clicks stay safe (#410). The Recents rows gain hover/focus actions: rename (inline edit — Enter/blur commits, Escape cancels) and delete (two-step arm/confirm), where delete also removes the entry's cached media — previously orphaned blobs accumulated forever. Rows sort by last-updated (newest first; freshly migrated entries follow alphabetically), and the active row's highlight now survives re-renders. Saving updates the active entry in place; renames do not bump `updated`, so they don't reorder. Quota errors surface as a dismissible notice above the list instead of a blocking alert(), and the empty-state message now shows whenever no transcripts exist (it used to be suppressed by unrelated keys such as the transcribe prefs). Unit tests cover the pure helpers via a fake Storage; the storage e2e spec grows migration, ordering, rename, delete, and active-highlight coverage.
… edited An entry that has never been edited since creation sorts by its creation date (updated || created); migration now stamps created (the true creation date was never recorded, so migration time is the proxy) so legacy entries order by date from then on instead of pooling alphabetically at the bottom. Ties and entries with no date at all still break alphabetically.
…e disclosure (#435) Every new document now lands in Recents automatically: hyperaudioInit is the hook — all five engines and the JSON/SRT/VTT import paths dispatch it, and neither the initial demo nor a Recents load does, so auto-add can never duplicate an existing entry. The entry is named after its media (local file's real name via the #426 mediaRef stamp; decoded URL basename for remote), always as a NEW entry — a repeat transcription coexists via the " (2)" suffix instead of overwriting. Edits autosave to the active entry, debounced 2s, from both the transcript (contenteditable) and the caption editor inputs. Autosaves skip re-encoding the media blob when document+source are unchanged, so typing doesn't rewrite a large IndexedDB entry every pause. Caption data is only stored when the track actually holds a data: URL — at auto-add time the track has been reset and not yet regenerated, and storing the empty src would have broken the load path (it now falls into its regenerate branch instead). The first time something autosaves, a one-time dismissible info notice explains that transcripts stay in this browser on this device and are managed in Recents — disclosure and control (rename/delete) rather than an upfront permission gate, which would mostly retrain users to click through it and, if declined, reintroduce the lost-transcription foot-gun this exists to fix.
The menu's default 8px padding put row pills 8px left of the notice box — both now sit on a 16px inset. The active row swaps daisyUI's near-black menu pill (suddenly prominent now that auto-add marks the new entry active immediately) for a quiet base-200 fill + semibold.
…pics The engines dispatch hyperaudioInit BEFORE regenerating captions, so at auto-add time the caption track — and the summary/topics panels — still hold the PREVIOUS document's content (the intro demo on a fresh session). The save guard only rejected an empty track, so a stale but valid data: URL was stamped into fresh entries, and loading one replayed the demo's captions. An auto-added entry now stores no derived state at all: captions regenerate from the transcript on load, and the first edit-autosave captures the real ones. Also: the row hover tooltip is only set when an entry actually has a summary or topics (it used to render just "Topics:" for entries with neither, plus a literal "..." placeholder before first hover).
…rrants a minor version bump, not another patch
…a into entries Deleting the entry that is loaded keeps the document on screen (the only undo there is) but silently stopped autosave — the old lost-work foot-gun through a side door. A sticky notice now says so and carries a Restore action: one click re-saves the on-screen document as a fresh entry under its old name, media included (a data: src goes back into IndexedDB; a blob: src re-saves through the normal path). The offer is withdrawn as soon as the screen holds a different document — a new transcription or another entry loaded — since restoring would then save the wrong content under the old name. This is also the groundwork for removing the explicit save/load dialogs (#436): recovery no longer needs them. Also: a document loaded from Recents has a base64 data: URL as its player src, and every save serialised that whole payload into the localStorage JSON — with debounced autosave that meant potentially megabytes rewritten on every pause in typing, straight toward the quota. The media already lives in IndexedDB under meta.mediaKey; saves now store an "indexeddb:" marker, which the load path treats like any non-http value.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #434, closes #435.
Summary
Recents becomes the primary workspace, working like the Claude app's sidebar: everything you transcribe or import is just there, named after its media, renamable, deletable, and kept up to date as you edit.
Storage model (#434)
hyperaudio:doc:<id>); the display name lives inmeta.namewithcreated/updatedtimestamps andmediaKey. Renaming is a one-field update, and saving a name that already exists gets a" (2)"suffix instead of silently overwriting.meta.mediaKey, so a rename never re-keys a large IndexedDB blob.<name>.hyperaudioentries migrate in place on first list render — same JSON, new key, name/mediaKey carried into meta,createdstamped at migration time so they participate in date ordering. Unparseable entries stay on their legacy key: listed, deletable, click-safe (storage: file list references entries by localStorage positional index — wrong file loaded (or TypeError) when keys shift; filename unescaped in list markup #410 guarantees preserved).Recents UI (#434)
alert(); the empty-state message no longer gets suppressed by unrelated localStorage keys.Auto-add + autosave (#435)
hyperaudioInit(which neither the initial demo nor a Recents load fires, so no duplicates). Default name: the media filename (local file's real name via the Interactive transcript export: pre-fill the media reference from a local file's real name #426 stamp; decoded URL basename for remote).hyperaudioInittime the caption track and summary/topics still hold the PREVIOUS document's content (the engines regenerate captions after the event), which was stamping the intro demo's captions into fresh entries. Captions regenerate from the transcript on load; the first edit-autosave captures the real ones. The same reasoning fixed a latent bug where an empty track src could be stored and break the load path.Notes
Testing
storage.test.mjscovers migration, collision suffixing, ordering, rename/delete semantics, media-name derivation against a fake Storage).