994528edb7d89068f371fef6d8a5fc72566f7334
lrnassar
  Fri Sep 4 13:35:16 2026 -0700
Adding an opt-in "Keep only last search" checkbox to the new BLAT search form. refs #36292

Behind a new hg.conf setting, blatOnlyLatestCheckbox (default off, documented in ex.hg.conf).
When checked, each new BLAT search removes the user's earlier BLAT result tracks in hgc's
buildBigPsl (cart variable blatOnlyLatest, remembered per browser in localStorage); unchecked
keeps the current accumulate behavior, so site defaults are unchanged. Deletion requires both
the hg.conf setting and the cart variable, so a stale cart cannot delete tracks where the
feature is off. Independent of, and coexists with, the blatOldTracks hg.conf setting.

diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js
index a4367586a6b..4f86984aaf0 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -648,43 +648,56 @@
 // like the C printInfoIcon, it relies on the string containing no double quotes to sit in a title=.
 var BLAT_GENOME_SEARCH_HELP =
     "All genome searches are case-insensitive.  Single-word searches default to prefix " +
     "matching if an exact match is not found. " +
     "<ul id='searchTipList' class='noBullets'>" +
     "<li> Force inclusion: Use a + sign before <b>+word</b> to ensure it appears in result.</li>" +
     "<li> Exclude words: Use a - sign before <b>-word</b> to exclude it from the search result.</li>" +
     "<li> Wildcard search: Add an * (asterisk) at end of <b>word*</b> to search for all terms starting with that prefix.</li>" +
     "<li> Phrase search: Enclose 'words in quotes' to search for the exact phrase.</li>" +
     "</ul>";
 
 // Cross-session memory of the "Keep results" checkbox.  A plain '1'/'0' string under one key;
 // wrapped in try/catch because localStorage throws in private-mode / disabled-storage browsers, in
 // which case we simply fall back to the cart-supplied default and skip persistence.
 var BLAT_KEEP_RESULTS_KEY = 'blatKeepResults';
+var BLAT_ONLY_LATEST_KEY = 'blatOnlyLatest';
 
 function blatGetKeepResultsPref() {
     // Returns true/false for a stored preference, or null if the user has never set one here.
     try {
         var v = localStorage.getItem(BLAT_KEEP_RESULTS_KEY);
         return v === null ? null : (v === '1');
     } catch (e) { return null; }
 }
 
 function blatSetKeepResultsPref(on) {
     try { localStorage.setItem(BLAT_KEEP_RESULTS_KEY, on ? '1' : '0'); } catch (e) { /* ignore */ }
 }
 
+function blatGetOnlyLatestPref() {
+    // Returns true/false for a stored preference, or null if the user has never set one here.
+    try {
+        var v = localStorage.getItem(BLAT_ONLY_LATEST_KEY);
+        return v === null ? null : (v === '1');
+    } catch (e) { return null; }
+}
+
+function blatSetOnlyLatestPref(on) {
+    try { localStorage.setItem(BLAT_ONLY_LATEST_KEY, on ? '1' : '0'); } catch (e) { /* ignore */ }
+}
+
 function blatOpts(list, cur) {
     return list.map(function(v) {
         return `<option value="${htmlEncode(v)}"${v === cur ? ' selected' : ''}>${htmlEncode(v)}</option>`;
     }).join('');
 }
 
 function blatFormCount() {
     // Live character count under the textarea.  Only these two nodes are touched on input - the
     // textarea itself is never re-rendered, so the caret stays where the user put it.
     var ta = document.getElementById('blatUserSeq');
     var out = document.getElementById('blatCountText');
     if (!ta || !out) { return; }
     var n = ta.value.replace(/[^A-Za-z*]/g, '').length;
     out.textContent = blatFmt(n) + ' of 25,000 characters';
     $('#blatLimitLink').toggleClass('over', n > 25000);
@@ -853,30 +866,35 @@
         `<span class="blatCheck"><label><input type="checkbox" name="${name}" ` +
         `id="blat_${name}"${on ? ' checked' : ''}>${label}</label>` +
         `<span class="blatInfo" title="${htmlEncode(tip)}">${BLAT_INFO_SVG}</span></span>`;
 
     // "Keep results" starting state.  The cart (cfg.keepResults) only remembers the choice within a
     // session; localStorage carries it across sessions so a user who wants their BLAT results to
     // accumulate does not have to re-tick the box on every visit.  localStorage wins when set (it is
     // the more durable record of the user's own preference); the cart is the fallback for a browser
     // that has never stored one.  Only consulted where the box is actually shown (blatOldTracks=
     // delete); elsewhere the choice has no effect, so there is nothing worth persisting.
     var keepResultsInit = cfg.keepResults;
     if (cfg.showKeepResults) {
         var storedKeep = blatGetKeepResultsPref();
         if (storedKeep !== null) { keepResultsInit = storedKeep; }
     }
+    var onlyLatestInit = cfg.onlyLatest;
+    if (cfg.showOnlyLatest) {
+        var storedLatest = blatGetOnlyLatestPref();
+        if (storedLatest !== null) { onlyLatestInit = storedLatest; }
+    }
 
     document.getElementById('blatFormBox').innerHTML =
         banner +
         '<div class="blatFormGrid"><div>' +
 
         '<div class="gbSection">Search &ndash; type keywords to find the target assembly</div>' +
         '<div class="blatRow">' +
             '<div class="blatField blatGenomeSlot"><span>Genome or assembly ' +
                 `<span class="blatInfo" title="${BLAT_GENOME_SEARCH_HELP}">${BLAT_INFO_SVG}</span>` +
                 '</span>' +
                 '<div id="blatGenomeSlot"></div></div>' +
             `<label class="blatField"><span>Query type</span><select name="type">${blatOpts(cfg.types, cfg.type)}</select></label>` +
             // Sort and output are submitted but not offered: sorting by anything other than score
             // is rarely useful, and this page always wants the hyperlink (results table) output.
             // Kept as hidden fields so the request hgBlat receives is unchanged.
@@ -907,30 +925,43 @@
                 `${BLAT_INFO_SVG}</span></span>` : '') +
             check('autoRearr', cfg.autoRearr, 'Show rearrangements',
                 'Shows duplications of the query sequence using multiple lines with connecting lines ' +
                 'between fragments, and displays inversions better (the "snakes" display). Can also ' +
                 'be switched on or off from the BLAT track configuration page.') +
             check('allResults', cfg.allResults, 'No min. score',
                 'Turns off minimum-match filtering so every alignment is returned. A human DNA search ' +
                 'normally requires 20 matching bases, based on the genome size, to filter out ' +
                 'lower-quality results; useful for short queries and the tiny genomes of ' +
                 'microorganisms.') +
             check('allGenomes', cfg.allGenomes, 'Search many genomes',
                 'Runs the same query against every default assembly and attached hub that has a ' +
                 'dedicated BLAT server. Dynamic BLAT servers are skipped and listed as such in the ' +
                 "output. See our <a target='_blank' href='../FAQ/FAQblat.html#blat9'>BLAT All FAQ</a> " +
                 'for more information.') +
+            // "Keep only last search" (RM #38086): submitted through a hidden field for the same
+            // reason as "Keep results" above: an unchecked box sends nothing, so the cart could
+            // never see it switched back off.
+            (cfg.showOnlyLatest ?
+                '<span class="blatCheck">' +
+                `<input type="hidden" name="blatOnlyLatest" id="blatOnlyLatestVal" value="${onlyLatestInit ? 1 : 0}">` +
+                `<label><input type="checkbox" id="blat_onlyLatest"${onlyLatestInit ? ' checked' : ''}>` +
+                'Keep only last search</label>' +
+                `<span class="blatInfo" title="${htmlEncode(
+                    'Each new BLAT search removes your earlier BLAT result tracks, so only the ' +
+                    'newest search shows in the Genome Browser. Leave unchecked to keep all ' +
+                    'results. Your choice is remembered for next time.')}">` +
+                `${BLAT_INFO_SVG}</span></span>` : '') +
         '</div>' +
 
         '<div class="gbSection">Query sequence</div>' +
         '<div class="blatTabs">' +
             '<button type="button" class="blatTab on" id="blatTabPaste">Paste sequence</button>' +
             '<button type="button" class="blatTab" id="blatTabUpload">Upload file</button>' +
         '</div>' +
 
         '<div id="blatPanePaste">' +
             '<div class="blatPaneHint"><span>Separate multiple sequences with a &gt;name line. ' +
             'Up to 25 sequences.</span>' +
             `<a href="#" id="blatExample">${htmlEncode(cfg.exampleLabel)}</a></div>` +
             '<textarea class="blatSeq" name="userSeq" id="blatUserSeq" spellcheck="false" ' +
             'aria-label="Paste in a query sequence"></textarea>' +
             '<div class="blatCount"><span id="blatCountText"></span>' +
@@ -973,30 +1004,34 @@
     var genomeInput = document.getElementById('genomeSearch');
     if (genomeInput && cfg.dbLabel) { genomeInput.value = cfg.dbLabel; }
 
     // Restore the sequence from the cart without going through innerHTML (avoids re-escaping).
     document.getElementById('blatUserSeq').value = cfg.userSeq || '';
     blatFormCount();
 
     $('#blatUserSeq').on('input', blatFormCount);
     // Mirror the "Keep results" checkbox into its hidden field so an unticked box submits an
     // explicit 0 rather than nothing at all, and remember the choice in localStorage so it comes
     // back pre-set on the user's next visit (see keepResultsInit above).
     $('#blat_keepResults').on('change', function() {
         document.getElementById('blatKeepResultsVal').value = this.checked ? '1' : '0';
         blatSetKeepResultsPref(this.checked);
     });
+    $('#blat_onlyLatest').on('change', function() {
+        document.getElementById('blatOnlyLatestVal').value = this.checked ? '1' : '0';
+        blatSetOnlyLatestPref(this.checked);
+    });
     $('#blatTabPaste').on('click', function() { blatFormTab(false); });
     $('#blatTabUpload').on('click', function() { blatFormTab(true); });
     // The example sequence is a real 2.5 kb query, fetched on demand so it is not carried in every
     // page load.  The link doubles as its own status indicator while the request is in flight.
     $('#blatExample').on('click', function(ev) {
         ev.preventDefault();
         var link = this;
         var label = cfg.exampleLabel;
         link.textContent = 'Loading example…';
         fetch(cfg.exampleUrl)
             .then(function(resp) {
                 if (!resp.ok) { throw new Error('HTTP ' + resp.status); }
                 return resp.text();
             })
             .then(function(fa) {