f48fc325d72424177878c4849096c77e06c2d3b0
max
  Mon Aug 17 07:34:42 2026 -0700
BLAT results: own track group, Delete-all button, clearer names, refs #38086

Behind hg.conf blatResultsGroup (default off, a release gate):

- BLAT result custom tracks go into their own "BLAT Results" track group
instead of the generic Custom Tracks group.
- That group's header gets a "Delete all" button that removes every BLAT
result track at once, so users are not stuck deleting them one by one.
- Headerless queries are named by query size + the top hit's gene, e.g.
"360bp SOD1", instead of the useless "blat YourSeq"; the date goes in
the longLabel.

hgBlat.c       getCustomName naming + blatDateStamp/topHitLocusLabel helpers
hgc.c          buildBigPsl tags the results track group=blat
customFactory.c checkGroup accepts the synthetic "blat" group
hgTracks.c     synthesize the BLAT Results group; Delete-all button; keep
the per-track delete icon for the new group
hgTracks.js    deleteAllBlatTracks()
hgCustom.c     hgct_do_delete_blat action (removes all blatResult=on tracks)
hgConfCatalog.py  register blatResultsGroup as a release gate

diff --git src/hg/hgBlat/hgBlat.c src/hg/hgBlat/hgBlat.c
index 5fae564a6b7..972bedda3f9 100644
--- src/hg/hgBlat/hgBlat.c
+++ src/hg/hgBlat/hgBlat.c
@@ -1412,53 +1412,149 @@
         {
         if (startsWith(customName, ct->tdb->track))
             // Found a track with this name.
             break;
         }
 
     if (ct == NULL)
         break;
 
     safef(buffer, sizeof buffer, "%s (%d)", name, count + 1);
     }
 
 return cloneString(buffer);
 }
 
+static char *blatDateStamp()
+/* Return today's date as YYYY-MM-DD in a static buffer (same idiom as cart.c). */
+{
+static char buf[16];
+time_t now = time(NULL);
+struct tm *tm = localtime(&now);
+strftime(buf, sizeof buf, "%Y-%m-%d", tm);
+return buf;
+}
+
+static char *topHitLocusLabel(char *database, struct psl *psl)
+/* Return a short, human-recognizable label for the top BLAT hit: the first gene symbol from the
+ * assembly's locusName table when the hit lands in a gene, otherwise a chrom:start position.
+ * The locusName lookup mirrors the results page (see printBlatResultsApp).  Returns a cloneString'd
+ * value the caller must free. */
+{
+struct subText *subList = NULL;
+struct sqlConnection *locusConn = blatLocusConn(database, &subList);
+char *label = NULL;
+if (locusConn != NULL)
+    {
+    struct sqlResult *sr = hRangeQuery(locusConn, "locusName", psl->tName, psl->tStart, psl->tEnd,
+        NULL, 0);
+    char **row = sqlNextRow(sr);
+    if (row != NULL)
+        {
+        char *raw = row[4];
+        char *genes = NULL;
+        // Only ex:/in: (exon/intron) hits name a gene; ig: (intergenic) just lists the neighbors,
+        // so for those fall through to coordinates, which are more useful.
+        if (startsWith("ex:", raw))
+            genes = raw + 3;
+        else if (startsWith("in:", raw))
+            genes = raw + 3;
+        if (genes != NULL)
+            {
+            char *dupe = cloneString(genes);
+            char *words[128];
+            int n = chopByChar(dupe, '|', words, ArraySize(words));
+            if (n > 0 && words[0][0] != '\0')
+                label = cloneString(words[0]);
+            freeMem(dupe);
+            }
+        }
+    sqlFreeResult(&sr);
+    hFreeConn(&locusConn);
+    }
+if (label == NULL)
+    {
+    char buf[128];
+    safef(buf, sizeof buf, "%s:%d", psl->tName, psl->tStart + 1);
+    label = cloneString(buf);
+    }
+return label;
+}
+
 static void getCustomName(char *database, struct cart *cart, struct psl *psl, char **pName, char **pDescription)
 // Find a track name that isn't currently a custom track. Also fill in description.
 {
 struct slName *names = namesInPsl(psl);
 char shortName[4096];
 char description[4096];
-
 unsigned count = slCount(names);
+
+// The improved naming (query size + top-hit gene, date in the description) rides along with the
+// BLAT Results track group, so it is behind the same hg.conf gate; off restores the original names.
+if (!cfgOptionBooleanDefault("blatResultsGroup", FALSE))
+    {
     if (count == 1)
         {
         safef(shortName, sizeof shortName, "blat %s", names->name);
         safef(description, sizeof description, "blat on %s", names->name);
         }
     else if (count == 2)
         {
         safef(shortName, sizeof shortName, "blat %s+%d", names->name, count - 1);
         safef(description, sizeof description, "blat on %d queries (%s, %s)", count, names->name, names->next->name);
         }
     else
         {
         safef(shortName, sizeof shortName, "blat %s+%d", names->name, count - 1);
         safef(description, sizeof description, "blat on %d queries (%s, %s, ...)", count, names->name, names->next->name);
         }
+    }
+else
+    {
+    // The short label (track name) drops the "BLAT" word - these tracks already live in the "BLAT
+    // Results" group, so the prefix would just be noise.  The long label keeps "BLAT" for context
+    // wherever it shows without the group heading (e.g. the custom-track manager, mouseovers).
+    char *date = blatDateStamp();
+    if (count == 1)
+        {
+        if (differentString(names->name, "YourSeq"))
+            {
+            // Query carried a FASTA header, so name the track after the sequence.
+            safef(shortName, sizeof shortName, "%s", names->name);
+            safef(description, sizeof description, "BLAT %s, %dbp, %s", names->name, psl->qSize, date);
+            }
+        else
+            {
+            // Headerless query: "YourSeq" tells the user nothing, so name it by query size and the
+            // top hit's gene (or position), which is what they actually recognize later.
+            char *locus = topHitLocusLabel(database, psl);
+            safef(shortName, sizeof shortName, "%dbp %s", psl->qSize, locus);
+            safef(description, sizeof description, "BLAT %dbp %s, %s", psl->qSize, locus, date);
+            freeMem(locus);
+            }
+        }
+    else if (count == 2)
+        {
+        safef(shortName, sizeof shortName, "%s+%d", names->name, count - 1);
+        safef(description, sizeof description, "BLAT %d queries (%s, %s), %s", count, names->name, names->next->name, date);
+        }
+    else
+        {
+        safef(shortName, sizeof shortName, "%s+%d", names->name, count - 1);
+        safef(description, sizeof description, "BLAT %d queries (%s, %s, ...), %s", count, names->name, names->next->name, date);
+        }
+    }
 
 *pName = makeNameUnique(shortName, database, cart);
 *pDescription = cloneString(description);
 }
 
 void queryServer(char *host, char *port, char *db, struct dnaSeq *seq, char *type, char *xType,
     boolean complex, boolean isProt, boolean queryRC, int seqNumber, char *genomeDataDir)
 /* Send simple query to server and report results. (no, it doesn't do this)
  * queryRC is true when the query has been reverse-complemented */
 {
 
 struct genomeHits *gH;
 AllocVar(gH);
 
 gH->host=cloneString(host);