feat(search): viewer progress + completed badges; dropdown to templ
Search results now show where you are in a book: a completed check at the 0.999 threshold (same cut as the filter status arms), a partial progress bar along the cover while mid-read, nothing when you never opened it. Rendered in BookCard (any grid whose adapter loads viewer progress) and in the header-search dropdown rows. The progress rides a LEFT JOIN reading_progress on the SAME user_id param SearchMediaItemsUnified already binds for library visibility — one requester param, no new wire inputs, row-neutral join (the table is UNIQUE per media_item+user). SearchMediaItemsUnifiedRow and BookInfo gain the keys additively (viewer_percentage null when unknown, library_name empty on feeds that don't load it); contract_pins.md carries the changelog entry and the pin tests widen in lockstep. The header dropdown itself migrates off the innerHTML string renderer in search.ts (a rendering-ownership violation: a templ twin existed via the HX-Request branch). The input now hx-gets layout=dropdown and swaps the new templ SearchResults partial — titles/authors highlighted server-side (escape-first, safe by construction), rows with the same progress overlays — while a slim Alpine component owns only client-local state (min-length gating, open/close, keyboard selection, htmx lifecycle). web/src/rendering-allowlist.txt drops the search.ts baseline to zero accordingly. Integration test pins the per-user join (fresh user sees 1.0/0.42/null on the fixture; the shared admin sees null everywhere — read-only) and the dropdown partial's overlays/count/row count.
This commit is contained in:
1 parent
d1f9ad3b25
commit
61e5f47ec0
18 files changed
+1140
-616
No files matched your search
@@ -452,9 +452,13 @@ func TestContractPin_KoboSyncFromServer(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// bookInfoKeys is the BookInfo JSON every card surface emits.
|
||||
// 2026-10-27 (search-progress batch): + viewer_percentage (nil = no
|
||||
// viewer data; only the search-unified adapter populates it today)
|
||||
// and + library_name (same deal). docs/contract_pins.md changelog.
|
||||
var bookInfoKeys = []string{
|
||||
"media_item_id", "title", "author", "cover_image_path",
|
||||
"has_conflict", "format_group", "badge",
|
||||
"viewer_percentage", "library_name",
|
||||
}
|
||||
|
||||
func collectionURL(setup *TestServerSetup, id string) string {
|
||||
@@ -667,6 +671,12 @@ func TestContractPin_SearchJSON(t *testing.T) {
|
||||
// (docs/contract_pins.md changelog).
|
||||
"subtitle", "explicit", "authors", "authors_search",
|
||||
"publishers", "publishers_search", "narrators_search",
|
||||
// 2026-10-27 (search-progress batch): + viewer_percentage —
|
||||
// the requester's reading_progress.percentage (LEFT JOIN on
|
||||
// the same user_id param the visibility join binds), null
|
||||
// when they never opened the book (docs/contract_pins.md
|
||||
// changelog).
|
||||
"viewer_percentage",
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/media-items/search?q=%s&library_id=%s", setup.Server.URL, url.QueryEscape(fx.itemTitle), fx.libraryID)
|
||||
|
||||
Reference in new issue
Block a user