66ea6cb4eaf2036e464be55b295765ed5105a0fb
max
  Wed Apr 22 09:42:25 2026 -0700
hgTrackUi/hui: render filter UI on supertrack configuration pages

Supertracks (group of tracks with superTrack on, no data of their own)
previously had no way to expose a shared filter: their trackDb stanza
can declare filter.* / filterByRange.* / filterValues.*, but those
settings were never drawn on the supertrack's hgTrackUi page. So users
had to open each subtrack's own configuration page and set the same
filter there, and the "lrSv.filter.svLen" cart namespace went unused.

This change wires that up:

- hgTrackUi.c (superTrackUi): after listing the subtracks, if the
supertrack tdb declares any filter.* settings call scoreCfgUi() to
render the standard filter UI. Cart variables land under the
supertrack's own name (e.g. "lrSv.filter.svLen.min"), and subtracks
already inherit them through cartOptionalStringClosestToHome()
walking tdb->parent. Subtrack-level values continue to override.

- hui.c:
- buildFilterBy() / filterByValues(): tolerate a NULL autoSql object,
so supertracks (which have no data table) don't errAbort when they
declare filterValues.* of virtual aggregated fields. Missing-field
errAbort still fires in the normal subtrack case.
- scoreCfgUi() / cfgByCfgType(): when called with title == NULL (the
supertrack filter path), suppress the default "<p>" separator and
the "<BR>" between the title bar and the filter block; the caller
renders its own section heading.
- asForTdb(): handle conn == NULL by returning NULL rather than
crashing, since supertrack filter rendering has no associated
sqlConnection.

refs #37426

diff --git src/hg/lib/hui.c src/hg/lib/hui.c
index cac6a74ed83..9ce7959c8b6 100644
--- src/hg/lib/hui.c
+++ src/hg/lib/hui.c
@@ -4001,34 +4001,37 @@
 
 static filterBy_t *buildFilterBy(struct trackDb *tdb, struct cart *cart, struct asObject *as, struct trackDbFilter *tdbFilter, char *name)
 /* Build a filterBy_t structure from a <column>FilterValues statement. */
 {
 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 = asColumnFind(as, field);
+struct asColumn *asCol = (as != NULL) ? asColumnFind(as, field) : NULL;
 if (asCol != NULL)
     filterBy->title = asCol->comment;
-else
+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);
 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);
@@ -4040,33 +4043,35 @@
     {
     char *setting = getFilterValueDefaultsSetting(cart, tdb, field);
     filterBy->slChoices = slNameListFromCommaEscaped(setting);
     }
 
 struct dyString *dy = dyStringNew(128);
 dyStringPrintf(dy, "%s.%s.%s", name, isHighlight ? "highlightBy": "filterBy", filterBy->column);
 filterBy->htmlName = dy->string;
 
 return filterBy;
 }
 
 filterBy_t *filterByValues(struct trackDb *tdb, struct cart *cart, struct trackDbFilter *trackDbFilters, char *name)
 /* Build a filterBy_t list from tdb variables of the form *FilterValues */
 {
+// Not every tdb has an autoSql: superTracks and tracks pointing at a
+// bigData file that isn't reachable at UI time both return NULL here.
+// That's fine for filterValues.* settings as long as a filterLabel.*
+// override is provided; buildFilterBy() already tolerates a NULL `as`.
 struct asObject *as = asForTdb(NULL, tdb);
-if (as == NULL)
-    errAbort("Track %s: Unable to get autoSql for %s", tdb->track, name);
 filterBy_t *filterByList = NULL, *filter;
 struct trackDbFilter *fieldFilter;
 while ((fieldFilter = slPopHead(&trackDbFilters)) != NULL)
     {
     if ((filter = buildFilterBy(tdb, cart, as, fieldFilter, name)) != NULL)
         slAddHead(&filterByList, filter);
     }
 return filterByList;
 }
 
 filterBy_t *filterBySetGetGuts(struct trackDb *tdb, struct cart *cart, char *name, char *subName, char *settingName)
 // Gets one or more "filterBy" settings (ClosestToHome).  returns NULL if not found
 {
 // first check to see if this tdb is using "new" FilterValues cart variables
 if (differentString(subName, "highlightBy"))
@@ -5935,32 +5940,33 @@
     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 );
-else
-    printf("<p>");
+// 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.
 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;
@@ -6905,30 +6911,34 @@
 }
 
 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)
@@ -10301,30 +10311,34 @@
 char *db = sqlGetDatabase(conn);
 int exists =  hashIntValDefault(hash, db, -1);
 if (exists < 0)
     {
     exists = sqlTableExists(conn, "tableDescriptions");
     hashAddInt(hash, db, exists);
     }
 return (boolean)exists;
 }
 
 struct asObject *asFromTableDescriptions(struct sqlConnection *conn, char *table)
 // If there is a tableDescriptions table and it has an entry for table, return
 // a parsed autoSql object; otherwise return NULL.
 {
 struct asObject *asObj = NULL;
+// Callers occasionally invoke asForTdb with conn=NULL (e.g. superTrack filter
+// rendering that isn't tied to a data table). Nothing to look up in that case.
+if (conn == NULL)
+    return NULL;
 if (tableDescriptionsExists(conn))
     {
     char query[PATH_LEN*2];
     // Try unsplit table first.
     sqlSafef(query, sizeof(query),
              "select autoSqlDef from tableDescriptions where tableName='%s'", table);
     char *asText = sqlQuickString(conn, query);
     // If no result try split table.
     if (asText == NULL)
         {
         sqlSafef(query, sizeof(query),
                  "select autoSqlDef from tableDescriptions where tableName='chrN_%s'", table);
         asText = sqlQuickString(conn, query);
         }
     if (isNotEmpty(asText))