feat(filters+nav): author navigation end-to-end — arrays on every media response, deep-linkable bookshelf filters, clickable authors

Completes the A2c acceptance scenario: click an author on a book
detail page -> land on the bookshelf filtered to everything that
author wrote (including their share of co-authored books).

- Media detail + unscoped list resolved maps expose subtitle,
  explicit, authors, publishers (the library-scoped raw branch and
  the search endpoint already did — raw rows follow the SELECT).
- SearchMediaItemsUnified SELECT carries the new columns; the search
  endpoint serializes the row directly, so clients get them there too.
- book_detail.templ renders each author as a link to
  /bookshelf?author_filter=<name>.
- Bookshelf honors filter params in the URL on load (author_filter,
  series_filter, genre_filter, publisher_filter, tags_filter,
  narrator_filter, language_filter): pre-fills the form, applies it to
  the first grid load. Comma-joined values are multi-select via the
  server-side string_to_array split. Filtered views are shareable.

Pins widened: search rows (+7 keys), resolved keys (+4); changelog
entries in docs/contract_pins.md. Android follow-up plan written to
~/.opencode/plan/bookhoard-app-v2-compat.md.
This commit is contained in:
John O'Keefe committed 2026-10-19 16:16:58 -04:00
1 parent f94e7472b4
commit 7537063cb7
8 files changed
+851 -728

No files matched your search

+8
View File
@@ -844,7 +844,9 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
"title": item.Title,
"subtitle": utils.PgTextString(item.Subtitle),
"author": utils.PgTextString(item.Author),
"authors": item.Authors,
"isbn": utils.PgTextString(item.Isbn),
"description": utils.PgTextString(item.Description),
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
@@ -857,6 +859,8 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
"asin": utils.PgTextString(item.Asin),
"date_published": item.DatePublished.Time.Format("2006-01-02"),
"publisher": utils.PgTextString(item.Publisher),
"publishers": item.Publishers,
"explicit": item.Explicit,
"contributors": item.Contributors,
"language": utils.PgTextString(item.Language),
"edition": utils.PgTextString(item.Edition),
@@ -916,7 +920,9 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
"title": item.Title,
"subtitle": utils.PgTextString(item.Subtitle),
"author": utils.PgTextString(item.Author),
"authors": item.Authors,
"isbn": utils.PgTextString(item.Isbn),
"description": utils.PgTextString(item.Description),
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
@@ -929,6 +935,8 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
"asin": utils.PgTextString(item.Asin),
"date_published": item.DatePublished.Time.Format("2006-01-02"),
"publisher": utils.PgTextString(item.Publisher),
"publishers": item.Publishers,
"explicit": item.Explicit,
"contributors": item.Contributors,
"language": utils.PgTextString(item.Language),
"edition": utils.PgTextString(item.Edition),