feat(filter): label endpoint names id values for client-held trees

The SSR embed (65f257f3) covers trees the server renders; one path
still fell through to the flawed fallback — applying a SAVED filter
from a cold bookshelf. That page was rendered before any filter
existed (nothing embedded), and the fallback reused the suggest facet,
whose rows are MEDIA-CONSTRAINED: "feed in [A,B,C] AND title contains
Chapter" labeled only the feeds with a matching episode (the exact
live repro from the original bug report).

- GET /api/filter/labels?field=X&ids=a,b,c resolves names straight
  from the entity tables via the shared ResolveFieldLabels core
  (extracted from FilterTreeLabels): no media join, no tree
  constraint. RLS/visibility ride the request identity — a
  zero-episode subscribed feed resolves its title (pinned; the facet
  path structurally could not), an unsubscribed feed id stays unnamed
  (pinned), non-id fields are 400 (pinned), ids cap at 100.
- Client: resolveIdLabels/resolveLabelsAsync use ONLY the endpoint;
  the quiet suggest-fetch mode is deleted (its only caller was the
  flawed fallback). The saved-filter apply flow resolves through the
  endpoint and rebuilds the chips bar when labels land — chips render
  raw ids only while the round trip is in flight, never stuck.
- 188 web tests green; Go endpoint pins + filter/episode slices green;
  contract_pins documents the endpoint and the fallback removal.
This commit is contained in:
John O'Keefe committed 2026-11-01 13:40:25 -05:00
1 parent a20741ccc9
commit 96a40f7d88
8 files changed
+274 -78

No files matched your search

+25
View File
@@ -248,6 +248,31 @@ func NewMediaHandler(db *database.Queries, raw database.DBTX, libraryService *se
// resolve id labels through it — 2026-11-01).
func (h *MediaHandler) SearchService() *services.SearchService { return h.searchService }
// ResolveFilterLabels — GET /api/filter/labels?field=X&ids=a,b,c names
// id-kind filter values straight from their source tables: the client
// path that never touches a server render (applying a SAVED filter
// from a cold bookshelf) would otherwise fall back to the suggest
// facet, whose rows are media-constrained and label only the ids whose
// media matches the tree's sibling rules (2026-11-01 feed repro).
// Visibility/RLS ride the request identity, same as the SSR embed.
func (h *MediaHandler) ResolveFilterLabels(c *echo.Context) error {
user := c.Get("user").(database.Users)
field := c.QueryParam("field")
if !services.IsIDValuedField(field) {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "field is not an id-valued filter field"})
}
ids := services.ParseUUIDValues(strings.Split(c.QueryParam("ids"), ","))
if len(ids) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "ids required"})
}
if len(ids) > services.MaxResolveIDs {
ids = ids[:services.MaxResolveIDs]
}
labels := h.SearchService().ResolveFieldLabels(
c.Request().Context(), user.ID, field, ids)
return c.JSON(http.StatusOK, map[string]interface{}{"labels": labels})
}
func (h *MediaHandler) ExecuteSearch(ctx context.Context, params services.SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
return h.searchService.ExecuteSearch(ctx, params)
}