Skip to content

Recents rework: stable-ID storage, rename/delete, auto-add + autosave - #438

Merged
maboa merged 7 commits into
mainfrom
enhancement/recents-stable-ids-434
Jul 29, 2026
Merged

Recents rework: stable-ID storage, rename/delete, auto-add + autosave#438
maboa merged 7 commits into
mainfrom
enhancement/recents-stable-ids-434

Conversation

@maboa

@maboa maboa commented Jul 29, 2026

Copy link
Copy Markdown
Member

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)

  • Entries are keyed by a stable generated ID (hyperaudio:doc:<id>); the display name lives in meta.name with created/updated timestamps and mediaKey. Renaming is a one-field update, and saving a name that already exists gets a " (2)" suffix instead of silently overwriting.
  • Cached local media keys off meta.mediaKey, so a rename never re-keys a large IndexedDB blob.
  • Legacy <name>.hyperaudio entries migrate in place on first list render — same JSON, new key, name/mediaKey carried into meta, created stamped 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).
  • Rows sort by last edit, with the creation date standing in for never-edited entries; ties break alphabetically.

Recents UI (#434)

  • Per-row hover/focus actions: rename (inline edit — Enter/blur commits, Escape cancels) and delete (two-step arm/confirm, auto-disarms). Delete also removes the entry's cached media — orphaned blobs used to accumulate forever.
  • Active row highlighted with a quiet fill (replacing daisyUI's near-black menu pill) and the highlight survives re-renders; rows truncate with ellipsis; list inset aligns with the notice above it.
  • Quota errors show a dismissible notice above the list instead of a blocking alert(); the empty-state message no longer gets suppressed by unrelated localStorage keys.

Auto-add + autosave (#435)

  • Every transcription and JSON/SRT/VTT import creates a new entry via 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).
  • Edits autosave debounced (2s) from both the transcript and the caption editor; media blobs are written once per document+source, not on every pause in typing.
  • Auto-add stores no derived state: at hyperaudioInit time 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.
  • One-time dismissible disclosure on first autosave ("stored in your browser, on this device only") — informational, not a permission gate.

Notes

  • Cache-busters for the changed storage JS and CSS point at 0.9.0 — this changes the storage format and saving model, warranting the minor bump.
  • The FILE-menu save/load dialogs still work (keyed by ID underneath); their simplification is Simplify the FILE-menu save/load dialogs once Recents autosaves #436.
  • Follow-up candidates noted in the issues: "Clear all", storage-size indicator/pruning.

Testing

maboa added 7 commits July 29, 2026 17:57
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.
@maboa
maboa merged commit 641666c into main Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant