Skip to content

Project library over OPFS: per-project working copies, silent Save, and the side-panel UI (#456) - #458

Merged
maboa merged 1 commit into
mainfrom
456-project-library
Aug 3, 2026
Merged

Project library over OPFS: per-project working copies, silent Save, and the side-panel UI (#456)#458
maboa merged 1 commit into
mainfrom
456-project-library

Conversation

@maboa

@maboa maboa commented Aug 3, 2026

Copy link
Copy Markdown
Member

Closes #456. Stacked on #404 (base: 403-hyperaudio-save) — the two merge and ship together as one release, so no released version carries the management-UI gap.

The model

  • Every project lives in work/<projectId>/ with a library.json index at the OPFS root. Identity is OPFS-native generated ids: none of the file↔workdir matching hazards, and re-opening the same .hyperaudio simply makes a second entry.
  • Save (⌘S / navbar) is a silent OPFS commit to saved.json — never a download. Autosave writes a separate draft.json scratch, so switching, closing or crashing loses nothing while the project honestly stays dirty (lastDraftAt > lastSavedAt) until a real Save. A future format revision records each manual save as a version.
  • Export Project (.hyperaudio) is a new FILE menu item and the only path that downloads. The native bridge still receives the container on Save; browsers without OPFS fall back to download-as-save.
  • Switching asks nothing and loses nothing — the outgoing draft flushes to its own directory; the discard dialogs die with the single work slot that required them.
  • Per-project Web Locks (hyperaudio:project:<id>, from Second-tab guard: one Web Lock, read-only banner (#403 Phase A) #450's slot): two tabs edit different projects; the same project in a second tab gets the guarded banner and queues for promotion. Index writes serialize under an origin-global lock; a BroadcastChannel keeps other tabs' panels honest.
  • Boot restores the most recently edited project (draft first), requests navigator.storage.persist(), and migrates the interim single-slot work/ layout once (never released — dev copies only).

The panel

Resurrected from pre-#451 history and rewired to the index: last-edited order, Starred pinned (#440 heading swap), current-project highlight, kebab menu — Info · Star · Rename · Duplicate · Delete (armed two-step) — and delete-current's Restore undo. Rename is the title Save and Export use. Hover popout right of the panel (1s dwell) with full name, summary and topics. Info is project-bound now: the modal shows name, media file + duration, stored provenance, summary and topics in uniform sections; apply() rebuilds the Transcription section per project (it previously showed the last engine run regardless of project — remaining report gaps in #457).

Also fixed

gather() now preserves class="speaker" via a targeted sanitizer — the blanket class strip meant every save/autosave demoted speaker labels to plain words (paragraphs lost speaker names; restored labels lost styling and the Speakers toggle). Latent since the Phase A writer; surfaced by the library's round-tripping.

Tests

60 unit / 71 e2e green. New library.spec.mjs covers switching-flush, star/rename/duplicate/delete/restore, boot restore, popout and the project-bound Info. Testing note: page.waitForFunction with an async predicate resolves immediately (pending Promise is truthy) — pollPage in helpers.mjs is the replacement; several phantom races traced to exactly that.

…nd the side-panel UI (#456)

The side panel returns as a project library over OPFS, and saving becomes
a silent commit — never a download.

Storage (hyperaudio-save.js):
- Every project lives in work/<projectId>/ with a library.json index at
  the OPFS root (name, starred, timestamps, media meta, summary/topics).
  Identity is OPFS-native generated ids — none of the file↔workdir
  matching hazards; re-opening the same .hyperaudio makes a second entry.
- Each project dir holds TWO states: saved.json, committed by Save
  (⌘S / the navbar button) as a SILENT OPFS write, and draft.json, the
  debounced autosave scratch. Dirty = lastDraftAt > lastSavedAt, so
  switching projects, closing or crashing loses nothing while the project
  honestly stays dirty until a real Save. A future format revision records
  each manual save as a version.
- Export Project (.hyperaudio) is a new FILE menu item and the only path
  that downloads. The native bridge still receives the container on Save,
  and no-OPFS browsers fall back to the download-as-save.
- Switching asks nothing and loses nothing: the outgoing draft flushes to
  its own directory first. The discard-on-switch dialogs die with the
  single work slot that required them.
- The #450 Web Lock becomes per-project (hyperaudio:project:<id>): two
  tabs edit different projects, the same project in a second tab gets the
  guarded banner and queues for promotion. Index writes serialize under
  an origin-global lock; a BroadcastChannel keeps other tabs' panels
  honest.
- Boot restores the most recently edited project (draft first) — the
  index replaces the localStorage boot hint — and requests
  navigator.storage.persist(). The interim single-slot work/ layout
  (never released) migrates once. The quit guard narrows to the one loss
  case left: a deleted project's document living only on screen.

Panel (hyperaudio-library.js, resurrected from pre-#451 history):
- Rows over the index: last-edited order, Starred pinned with the #440
  heading swap, current-project highlight, kebab menu (Info, star, inline
  rename, duplicate, armed two-step delete), delete-current's Restore
  undo. Rename IS the title Save and Export use — index, stored state
  files and live session all updated.
- Row-height square kebab hover, row-level hover state, 4px row gaps, and
  a hover popout floated right of the panel (1s dwell) with the full
  name, summary and topics.
- Info moved from the controls row into the kebab: the modal is
  project-bound — name, media file + duration, stored provenance,
  summary and topics in uniform sections. apply() rebuilds the
  Transcription section from stored provenance; it previously kept
  showing the last engine run regardless of project. Remaining
  transcription-report gaps are tracked in #457.

Fixed along the way: gather() now preserves class="speaker" via a
targeted sanitizer instead of the editor's blanket class strip — every
save/autosave had been demoting speaker labels to plain words, so
paragraphs lost their speaker names and restored labels lost their
styling and the Speakers toggle. Latent since the Phase A writer.

Tests: 60 unit / 71 e2e green. Note for future specs:
page.waitForFunction with an ASYNC predicate resolves immediately (the
pending Promise is truthy) — use pollPage in helpers.mjs; several
phantom races traced back to exactly that.

Closes #456.
@maboa
maboa changed the base branch from 403-hyperaudio-save to main August 3, 2026 10:18
@maboa
maboa merged commit 598104d into main Aug 3, 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

Development

Successfully merging this pull request may close these issues.

Project library: per-project OPFS working copies with the side-panel management UI

1 participant