bc236ffceb9ddc8632c872168c92c229206989af
max
  Mon Aug 10 08:25:30 2026 -0700
hgc BLAT alignment viewer: consistent headings, section reorder, friendlier expired-link message, refs #37893

- Sidebar links and section headings are now all sentence case, and each
sidebar link matches its heading (the Side-by-side heading is relabeled too).
- Reorder the modern single-page view to Query, Side-by-side, Genome so the
per-block jump links sit under Only genome sequence, where their anchors
actually are, rather than under Side-by-side; tighten the block-link spacing.
The reorder is done server-side (capture the shared library output via
open_memstream, emit the sections in the new order) so the page does not
reflow after it loads.
- htcUserAli now shows a friendly 'no longer available, run a new BLAT search'
message when the search's trash files have aged out, instead of a raw
file-open error.
- Free the open_memstream buffer with libc free(), not kent freeMem().

diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c
index 1f822180985..1e224043630 100644
--- src/hg/hgc/hgc.c
+++ src/hg/hgc/hgc.c
@@ -9092,31 +9092,31 @@
        ".blatBtn{padding:4px 12px; font-size:13px; border:1px solid #999; border-radius:3px;"
        " background:#e6e6e6; text-decoration:none; white-space:nowrap; cursor:pointer}"
        /* nice_menu.css sets a:link blue (specificity 0,1,1); a.blatBtn:link (0,2,1) beats it */
        "a.blatBtn:link, a.blatBtn:visited, a.blatBtn:hover{color:#000; text-decoration:none}"
        ".blatBtn:hover{background:#d8d8d8}"
        "#blatAlnBody{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color:#374a5e;"
        " display:grid; grid-template-columns:220px 1fr;"      /* full-height sidebar + content column */
        " background:#fff}"                                    /* edge to edge: no margin, no border */
        "#blatAlnBody a{color:#0a3a7a}"
        "#blatAlnBody a:hover{color:#8b1a1a}"
        "#blatAlnNav{grid-column:1; grid-row:1; background:#f4f7fb; border-right:1px solid #dde3ea}"
        /* keep the grey column full height, but pin the links so they stay visible while scrolling */
        "#blatAlnNavInner{position:sticky; top:0; padding:18px 20px; display:flex; flex-direction:column;"
        " gap:16px}"
        "#blatAlnNav a{font-weight:700; text-decoration:none}"   /* already obviously links; no underline */
-       "#blatAlnBlocks{display:flex; flex-direction:column; gap:8px; margin:2px 0 0 14px}"  /* block links, indented under side-by-side */
+       "#blatAlnBlocks{display:flex; flex-direction:column; gap:2px; margin:2px 0 0 14px}"  /* block links, tight list indented under genome sequence */
        "#blatAlnBlocks a{font-weight:400; font-size:13px}"
        "#blatAlnContent{grid-column:2; grid-row:1; min-width:0; padding:0 20px 14px}"
        "#blatAlnContent h2{display:none}"
        "#blatAlnContent hr{display:none}"
        "#blatAlnContent h4{margin:16px -20px 0; padding:8px 20px; background:#4c759c; color:#fff;"
        " font-size:15px; font-weight:700}"                     /* -20px: bar spans full content width */
        "#blatAlnContent h4:first-child{margin-top:0}"          /* Alignment Summary flush at top */
        "#blatAlnContent h4 a{color:#fff}"
        /* undo bootstrap.css (pulled in by webStartGbNoBanner's gbHeader) on the sequence blocks:
         * it would give <pre> a grey box, a border and word-break that mangles the alignment */
        "#blatAlnContent pre{margin:0; padding:2px 0 12px; line-height:1.4; background:none; border:0;"
        " border-radius:0; color:#374a5e; white-space:pre; word-break:normal; word-wrap:normal}"
        /* key-value summary strip, mirroring hgBlat's .blatStrip (label over value, thin dividers) */
        ".blatAlnStrip{display:flex; align-items:center; gap:24px; flex-wrap:wrap; margin:2px 0 14px}"
        ".blatAlnStat{display:flex; flex-direction:column; gap:1px}"
@@ -9130,99 +9130,122 @@
 printf("<div class='blatTitleBar'>");
 printf("<span class='blatTtl'>BLAT Base Alignment: %s</span>", blatAsmLabel(database));
 printf("<span class='blatBtns'>");
 printf("<a href='hgBlat?blatReopen=1&hgsid=%s' class='blatBtn'>"
        "\xe2\x80\xb9 Back to results</a>", cartSessionId(cart));
 if (canShare)
     printf("<a href='#' id='blatShareBtn' class='blatBtn'>Share a link</a>");
 printf("</span></div>\n");
 
 /* one white panel laid out as two grid columns: a full-height "jump to" sidebar on the left, and on
  * the right an "Alignment Summary" header, the summary line, and the base-by-base alignment inlined
  * so the whole page scrolls */
 printf("<div id='blatAlnBody'>\n");
 
 printf("<div id='blatAlnContent'>\n");
-printf("<h4>Alignment Summary</h4>\n");
+printf("<h4>Alignment summary</h4>\n");
 /* comma-format the coordinates and base counts, matching the new Table view (readable at the
  * hundreds-of-millions scale of genomic coordinates, and the convention elsewhere in the browser) */
 char tStartC[32], tEndC[32], matchC[32], qSizeC[32];
 sprintLongWithCommas(tStartC, psl->tStart + 1);
 sprintLongWithCommas(tEndC, psl->tEnd);
 sprintLongWithCommas(matchC, psl->match + psl->repMatch);
 sprintLongWithCommas(qSizeC, psl->qSize);
 /* key-value strip (Query / Position / Identity / Matches / Strand), styled like hgBlat's summary
  * strip so the two pages read as one design. */
 printf("<div class='blatAlnStrip'>"
        "<div class='blatAlnStat'><span class='k'>Query</span><span class='v'>%s</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Position</span><span class='v'>%s:%s-%s</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Identity</span>"
        "<span class='v' style='color:%s'>%.1f%%</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Matches</span><span class='v'>%s of %s</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Strand</span><span class='v'>%s</span></div>"
        "</div>\n",
        qName, chrom, tStartC, tEndC, idColor, ident, matchC, qSizeC, psl->strand);
 if (isNotEmpty(aliasStr))
     printf("<p>Genome sequence %s is also known as: %s.</p>\n", chrom, aliasStr);
 /* The shared library returns the number of alignment blocks it actually shows.  The DNA path merges
  * blocks separated by gaps <= 8 bases, so this can be fewer than psl->blockCount; use it (not
  * psl->blockCount) so the sidebar's "Block N" links match the #1..#N anchors that were emitted. */
 int blockCount;
+/* Capture the shared library's alignment HTML so we can reorder its sections for this page.  The
+ * library emits them as Query (#cDNA), Genome (#genomic), then Side-by-side (#ali), with the
+ * per-block anchors living inside the Genome section.  We want Query, Side-by-side, Genome so the
+ * long per-block list sits at the bottom of both the page and the sidebar.  Reorder here, on the
+ * server, rather than in JS, so the page does not reflow after it loads. */
+char *alnHtml = NULL;
+size_t alnLen = 0;
+FILE *alnF = open_memstream(&alnHtml, &alnLen);
 if (qType == gftRna || qType == gftDna)
-    blockCount = showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE);
+    blockCount = showPartialDnaAlignment(psl, oSeq, alnF, cdsS, cdsE, FALSE);
 else
-    blockCount = showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName);
+    blockCount = showGfAlignment(psl, oSeq, alnF, qType, qStart, qEnd, qName);
+fclose(alnF);
+char *pGenome = (alnHtml != NULL) ? stringIn("<H4><A NAME=genomic>", alnHtml) : NULL;
+char *pAli    = (alnHtml != NULL) ? stringIn("<H4><A NAME=ali>", alnHtml) : NULL;
+if (pGenome != NULL && pAli != NULL && pGenome < pAli)
+    {                                                /* Query, then Side-by-side, then Genome */
+    fwrite(alnHtml, 1, pGenome - alnHtml, stdout);   /* legend + Query (#cDNA) section */
+    fputs(pAli, stdout);                             /* Side-by-side (#ali) section, through footnote */
+    fwrite(pGenome, 1, pAli - pGenome, stdout);      /* Genome (#genomic) section, with block anchors */
+    }
+else
+    fputs((alnHtml != NULL) ? alnHtml : "", stdout);
+free(alnHtml);   /* libc free: open_memstream's buffer is malloc'd, not a kent needMem block */
 printf("</div>\n");   /* #blatAlnContent */
 
 /* Sidebar, emitted after the alignment so blockCount is known; CSS grid puts it back in column 1.
  * The inner div is position:sticky so the links stay in view as the long alignment scrolls. */
 printf("<div id='blatAlnNav'><div id='blatAlnNavInner'>\n");
 printf("<a href='#cDNA'>Only query sequence</a>\n"
-       "<a href='#genomic'>Only genome sequence</a>\n"
-       "<a href='#ali'>Side by Side Alignment</a>\n");
-if (blockCount > 1)   /* per-block jump links, indented under the side-by-side item */
+       "<a href='#ali'>Side by side alignment</a>\n"
+       "<a href='#genomic'>Only genome sequence</a>\n");
+if (blockCount > 1)   /* per-block jump links, indented under the genome-sequence item where their anchors live */
     {
     int bi;
     printf("<div id='blatAlnBlocks'>\n");
     for (bi = 1;  bi <= blockCount;  ++bi)
         printf("<a href='#%d'>Block %d</a>\n", bi, bi);
     printf("</div>\n");
     }
 printf("</div></div>\n");
 
 printf("</div>\n");   /* #blatAlnBody */
 
-/* The cDNA/Genomic section headers come from shared library code (fuzzyShow.c / pslShow.c) as
- * "cDNA <qName>" / "Genomic <chrom> :"; relabel them to the sidebar wording via JS (there is no C
- * hook for it), keeping the #cDNA/#genomic jump anchors.  qName and chrom are already sanitized. */
+/* The cDNA/Genomic/Side-by-side section headers come from shared library code (fuzzyShow.c /
+ * pslShow.c) as "cDNA <qName>" / "Genomic <chrom> :" / "Side by Side Alignment"; relabel them to
+ * the sidebar wording (sentence case) via JS (there is no C hook for it), keeping the
+ * #cDNA/#genomic/#ali jump anchors.  qName and chrom are already sanitized. */
 jsInlineF(
     "(function(){\n"
     "function relabel(anchor, text){\n"
     "  var a = document.getElementsByName(anchor);\n"
     "  if (a && a.length){\n"
     "    var h = a[0].parentNode;\n"
+    "    var star = /\\*\\s*$/.test(h.textContent) ? '*' : '';\n"  // keep the footnote marker if present
     "    h.textContent = '';\n"
     "    var k = document.createElement('a'); k.name = anchor; h.appendChild(k);\n"
-    "    h.appendChild(document.createTextNode(text));\n"
+    "    h.appendChild(document.createTextNode(text + star));\n"
     "  }\n"
     "}\n"
     "relabel('cDNA', 'Only query sequence: %s');\n"
     "relabel('genomic', 'Only genome sequence: %s');\n"
+    "relabel('ali', 'Side by side alignment');\n"   // match the sidebar wording and sentence case
     "})();\n",
     qName, chrom);
 
 /* "Share a link": save an anonymous session (hgSession API), build a durable hgc?g=htcBlatAlign link
  * that rebuilds THIS alignment from the session's durable bigPsl custom track (no BLAT re-run, no
  * stored trash sequence), and hand it to the shared "Share a link" modal (topLinks.js shareUrl,
  * loaded by the menu bar) so it looks like every other share dialog.  qName is already sanitized. */
 if (canShare)
     jsInlineF(
     "(function(){\n"
     "var btn = document.getElementById('blatShareBtn');\n"
     "if (!btn) return;\n"
     "btn.addEventListener('click', function(ev){\n"
     "  ev.preventDefault();\n"
     "  if (btn.dataset.busy) return;\n"
@@ -9260,30 +9283,37 @@
 boolean modern = cartUsualBoolean(cart, "blatNewPage", FALSE);
 
 char title[1024];
 safef(title, sizeof title, "User Sequence vs Genomic");
 if (modern)
     {
     char pageTitle[256];
     safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", blatAsmLabel(database));
     webStartGbNoBanner(cart, database, pageTitle);   // menubar + <main>, no legacy section tables
     }
 else
     htmlFramesetStart(title);
 
 start = cartInt(cart, "o");
 parseSs(fileNames, &pslName, &faName, &qName);
+if (modern && (!fileExists(pslName) || !fileExists(faName)))
+    {   /* the search's trash files have been cleaned up: a friendly note, not a raw file error */
+    printf("<p>This BLAT alignment is no longer available. The search results it came from have "
+           "expired. Please run a new <a href=\"hgBlat\">BLAT search</a>.</p>\n");
+    webEndGb();
+    exit(0);
+    }
 pslxFileOpen(pslName, &qt, &tt, &lf);
 isProt = (qt == gftProt);
 while ((psl = pslNext(lf)) != NULL)
     {
     if (sameString(psl->tName, seqName) && psl->tStart == start && sameString(psl->qName, qName))
         break;
     pslFree(&psl);
     }
 lineFileClose(&lf);
 if (psl == NULL)
     errAbort("Couldn't find alignment at %s:%d", seqName, start);
 oSeqList = faReadAllSeq(faName, !isProt);
 for (oSeq = oSeqList; oSeq != NULL; oSeq = oSeq->next)
     {
     if (sameString(oSeq->name, qName))