329cebf37d4a04c2a490121e21d166cab69e8562
max
  Sat Jul 25 18:22:02 2026 -0700
hui: warn when a numeric filter setting has no matching filter.<field>, refs #37927

A numeric range filter is only discovered from a filter.<field> (or
<field>Filter) setting. Emit a warning for orphaned filter settings that will
otherwise be silently ignored.

diff --git src/hg/lib/hui.c src/hg/lib/hui.c
index 03f819249e5..905060ba5c6 100644
--- src/hg/lib/hui.c
+++ src/hg/lib/hui.c
@@ -6949,41 +6949,81 @@
  * Try to guess what the user means. */
 {
 if (sameString("score", field))
     return 4;
 if (sameString("signal", field))
     return 6;
 if (sameString("signalValue", field))
     return 6;
 if (sameString("pValue", field))
     return 7;
 if (sameString("qValue", field))
     return 8;
 return -1;
 }
 
+static void warnOrphanedRangeFilters(struct trackDb *tdb)
+/* A numeric filter is only DISCOVERED from a filter.<field> (or <field>Filter)
+ * setting -- see FILTER_NUMBER_WILDCARD and tdbGetTrackNumFilters(). A
+ * filterByRange.<field> or filterLimits.<field> with no matching filter.<field>
+ * is silently ignored and its control never appears, which is an easy and
+ * confusing mistake to make. Warn so the track/hub developer sees why a filter
+ * they configured did not show up. */
+{
+struct slName *orphans = slCat(trackDbSettingsWildMatch(tdb, "filterByRange.*"),
+                               trackDbSettingsWildMatch(tdb, "filterLimits.*"));
+struct hash *seen = hashNew(0);
+struct slName *s;
+for (s = orphans; s != NULL; s = s->next)
+    {
+    char *field = strchr(s->name, '.');
+    if (field == NULL)
+        continue;
+    field++; // skip past the '.'
+    if (hashLookup(seen, field) != NULL)
+        continue;
+    hashAdd(seen, field, NULL);
+    char setting[512];
+    safef(setting, sizeof setting, "filter.%s", field);
+    if (trackDbSettingClosestToHome(tdb, setting) != NULL)
+        continue;
+    safef(setting, sizeof setting, "%s%s", field, FILTER_NUMBER_NAME_CAP);
+    if (trackDbSettingClosestToHome(tdb, setting) != NULL)
+        continue;
+    warn("Track %s: a filterByRange/filterLimits setting exists for field '%s' "
+         "but there is no filter.%s (the default range), so this filter is not "
+         "shown. Add 'filter.%s min:max' to enable it.",
+         tdb->track, field, field, field);
+    }
+hashFree(&seen);
+slFreeList(&orphans);
+}
+
 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);
+    warnOrphanedRangeFilters(tdb);
+    }
 if (trackDbFilters)
     {
     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);