24fece8377dedf184e8e872325ff2af175076552
max
  Mon May 4 05:51:05 2026 -0700
hui: fix duplicated filter UI on bigGenePred; tighten supertrack filter changes

Two issues, both surfaced reviewing #37426:

1. bigGenePred tracks (e.g. openprot) rendered every filterBy/highlightBy
<select> twice with the same id. dropdownchecklist.js binds the first
instance only, leaving the second copy display:none — users saw filter
labels under the second copy with no input fields under them. The
duplication was pre-existing (since 7d9e5c172d1b reorganized
genePredCfgUi/scoreCfgUi): genePredCfgUi rendered filterBy inline, then
for bigGenePred called scoreCfgUi which rendered filterBy again. Skip
the inline rendering for bigGenePred and let scoreCfgUi own it; plain
genePred (which doesn't reach scoreCfgUi here) is unchanged.

2. Tighten the supertrack filter changes from 66ea6cb4eaf:
- cfgBeginBoxAndTitle: revert the title==NULL "emit nothing" branch.
The supertrack flow exits scoreCfgUi before reaching this anyway,
so the change was only affecting other title==NULL callers like
interactUi.c — pure unintended side effect.
- numericFiltersShowAll: revert the title==NULL <BR> suppression for
the same reason. The supertrack <h3> heading already provides
enough separation.
- buildFilterBy: tighten the errAbort relaxation. The previous
condition (as != NULL && getLabelSetting == NULL) let a typo'd
filterValues.<field> on a regular subtrack silently render a
useless filter whenever a matching filterLabel.<field> was
present. Restrict the relaxation to as==NULL (the supertrack /
noData case). Add a warn() before errAbort so hub makers reading
the page see an actionable hint, not just an opaque abort.

refs #37426

diff --git src/hg/lib/hui.c src/hg/lib/hui.c
index 3304df6a7b9..fe7f06d8f3c 100644
--- src/hg/lib/hui.c
+++ src/hg/lib/hui.c
@@ -4004,35 +4004,44 @@
 {
 boolean isHighlight = startsWith("highlightValues.", tdbFilter->name);
 char *field = tdbFilter->fieldName;
 if (isEmpty(tdbFilter->setting))
     errAbort("track %s: FilterValues setting of field '%s' must have a value.", tdb->track, tdbFilter->fieldName);
 
 char *value = cartUsualStringClosestToHome(cart, tdb, FALSE, tdbFilter->name, tdbFilter->setting);
 
 filterBy_t *filterBy;
 AllocVar(filterBy);
 filterBy->column = cloneString(field);
 filterBy->title = cloneString(field); ///  title should come from AS file, or trackDb variable
 struct asColumn *asCol = (as != NULL) ? asColumnFind(as, field) : NULL;
 if (asCol != NULL)
     filterBy->title = asCol->comment;
-else if (as != NULL && getLabelSetting(cart, tdb, field) == NULL)
-    // Only error when there's an autoSql but the field is missing AND there's
-    // no filterLabel override. superTracks/noData tdbs have as==NULL and can
-    // legitimately declare filterValues on virtual fields they only aggregate.
-    errAbort("Track %s: Building filter on field %s which is not in AS file.", tdb->track, field);
+else if (as != NULL)
+    {
+    // We have autoSql but the field is missing — that's a typo or an
+    // outdated trackDb. Emit a visible warning so a hub maker reading the
+    // page sees a clear hint, then abort. (The as==NULL case is reserved
+    // for superTracks/noData tdbs that legitimately declare filterValues
+    // on virtual fields they only aggregate over.)
+    warn("Track %s: filter on field '%s' is not in the autoSql file. "
+         "Check the field name in your trackDb filterValues.* / "
+         "filterByRange.* settings against the .as schema.",
+         tdb->track, field);
+    errAbort("Track %s: Building filter on field %s which is not in AS file.",
+             tdb->track, field);
+    }
 char *trackDbLabel = getLabelSetting(cart, tdb, field);
 if (trackDbLabel)
     filterBy->title = trackDbLabel;
 filterBy->useIndex = FALSE;
 filterBy->slValues = slNameListFromCommaEscaped(value);
 chopUpValues(filterBy);
 if (cart != NULL)
     {
     char suffix[256];
     safef(suffix, sizeof(suffix), "%s.%s", isHighlight ? "highlightBy" : "filterBy", filterBy->column);
     boolean parentLevel = isNameAtParentLevel(tdb,tdb->track);
     if (cartLookUpVariableClosestToHome(cart,tdb,parentLevel,suffix,&(filterBy->htmlName)))
         {
         filterBy->slChoices = cartOptionalSlNameList(cart,filterBy->htmlName);
         freeMem(filterBy->htmlName);
@@ -5940,33 +5949,32 @@
     if (boxed)
         printf("<BR>");
     }
 if (boxed)
     {
     printf("<TABLE class='blueBox");
     char *view = tdbGetViewName(tdb);
     if (view != NULL)
         printf(" %s",view);
     printf("' style='background-color:%s;'><TR><TD>", COLOR_BG_ALTDEFAULT);
     if (title)
         printf("<CENTER><B>%s Configuration</B></CENTER>\n", title);
     }
 else if (title)
     printf("<p><B>%s &nbsp;</b>", title );
-// When !boxed and title==NULL, emit nothing: the caller (e.g. supertrack
-// filter UI) already renders its own heading and doesn't want a stray
-// empty paragraph.
+else
+    printf("<p>");
 return boxed;
 }
 
 void cfgEndBox(boolean boxed)
 // Handle end of box and title for individual track type settings
 {
 if (boxed)
     puts("</td></tr></table>");
 }
 
 void snakeOption(struct cart *cart, char *name, char *title, struct trackDb *tdb)
 /* let the user choose to see the track in snake mode */
 {
 if (!cfgOptionBooleanDefault("canSnake", TRUE))
     return;
@@ -6911,34 +6919,30 @@
 }
 
 static int numericFiltersShowAll(char *db, struct cart *cart, struct trackDb *tdb, boolean *opened,
                                  boolean boxed, boolean parentLevel,char *name, char *title,
                                  boolean isHighlight)
 // Shows all *Filter style filters.  Note that these are in random order and have no graceful title
 {
 int count = 0;
 struct trackDbFilter *trackDbFilters = NULL;
 if (isHighlight)
     trackDbFilters = tdbGetTrackNumHighlights(tdb);
 else
     trackDbFilters = tdbGetTrackNumFilters(tdb);
 if (trackDbFilters)
     {
-    // The <BR> is a separator under the track's "Configuration" block title.
-    // Callers that don't emit a title (e.g. the supertrack filter UI that
-    // owns its own heading) pass title==NULL and don't want the extra break.
-    if (title != NULL)
     puts("<BR>");
     struct trackDbFilter *filter = NULL;
     struct sqlConnection *conn = NULL;
     if (!isHubTrack(db))
         conn = hAllocConnTrack(db, tdb);
     struct asObject *as = asForTdb(conn, tdb);
     hFreeConn(&conn);
 
     while ((filter = slPopHead(&trackDbFilters)) != NULL)
         {
         char *field = filter->fieldName;
         char *scoreName = cloneString(filter->name);
         char *trackDbLabel = getLabelSetting(cart, tdb, field);
 
         if (as != NULL)
@@ -7940,52 +7944,61 @@
 if (trackDbSettingClosestToHomeOn(tdb, "nmdFilter"))
     {
     boolean nmdDefault = FALSE;
     safef(varName, sizeof(varName), "hgt.%s.nmdFilter", name);
     nmdDefault = cartUsualBoolean(cart,varName, FALSE);
     // TODO: var name (hgt prefix) needs changing before ClosesToHome can be used
     printf("<p><b>Filter out NMD targets.</b>");
     cgiMakeCheckBox(varName, nmdDefault);
     }
 
 if (!sameString(tdb->track, "tigrGeneIndex")
 &&  !sameString(tdb->track, "ensGeneNonCoding")
 &&  !sameString(tdb->track, "encodeGencodeRaceFrags"))
     baseColorDropLists(cart, tdb, name);
 
+// For bigGenePred, scoreCfgUi() (called below) renders the filterBy/highlightBy
+// controls itself. Rendering them here too produces two <SELECT> elements with
+// the same id, and dropdownchecklist.js only binds the first; the second copy
+// stays display:none and shows no input fields. So skip the inline pass for
+// bigGenePred and let scoreCfgUi own it.
+boolean isBigGenePred = startsWith("bigGenePred", tdb->type);
+if (!isBigGenePred)
+    {
     filterBy_t *filterBySet = filterBySetGet(tdb,cart,name);
     if (filterBySet != NULL)
         {
         printf("<BR>");
         filterBySetCfgUi(cart,tdb,filterBySet,FALSE, name);
         filterBySetFree(&filterBySet);
         }
     filterBy_t *highlightBySet = highlightBySetGet(tdb,cart,name);
     if (highlightBySet != NULL)
         {
         printf("<BR>");
         highlightBySetCfgUi(cart,tdb,highlightBySet,FALSE, name, TRUE);
         filterBySetFree(&highlightBySet);
         }
+    }
 
 squishyPackOption(cart, name, title, tdb);
 filterNameOption(cart, name, tdb);
 wigOption(cart, name, title, tdb);
 cfgEndBox(boxed);
 // N.B. scoreCfgUi maybe creates a box, so this is called after cfgEndBox
 // unclear what the logic is with box creation here
-if (startsWith("bigGenePred", tdb->type))
+if (isBigGenePred)
     {
     char *scoreMax = trackDbSettingClosestToHome(tdb, SCORE_FILTER _MAX);
     int maxScore = (scoreMax ? sqlUnsigned(scoreMax):1000);
     scoreCfgUi(db, cart,tdb,name,title,maxScore,boxed);
     }
 }
 
 static boolean isSpeciesOn(struct cart *cart, struct trackDb *tdb, char *species, char *option, int optionSize, boolean defaultState)
 /* check the cart to see if species is turned off or on (default is defaultState) */
 {
 boolean parentLevel = isNameAtParentLevel(tdb,option);
 if (*option == '\0')
     safef(option, optionSize, "%s.%s", tdb->track, species);
 else
     {