9a2fac6063097ddaa8f0e0063cd9c26c7ce07af7
max
  Fri Sep 18 08:11:31 2026 -0700
hgSession: address UI feedback on the new Sessions page

#Preview2 week - bugs introduced now will need a build patch to fix
Wraps the intro text in <p> tags, renames "Update now" to "Overwrite
now" with a visible info bubble, gives the name/description fields
outside labels and narrower boxes with a real generated name as the
name field's placeholder, moves "Back up custom tracks" out of the
Advanced panel to its own bar, and splits Advanced into Load/Save
columns.

refs #38311

diff --git src/hg/js/hgSession.js src/hg/js/hgSession.js
index fae84c8c90f..c5e18662372 100644
--- src/hg/js/hgSession.js
+++ src/hg/js/hgSession.js
@@ -1,22 +1,23 @@
 // hgSession.js - the experimental client-rendered "My Sessions" page.
 //
 // An opt-in modern alternative to the classic server-rendered hgSession page, applying hgBlat's
 // facelift strategy (#37996): hgSession.c emits the session list and page config as an inline JSON
 // global (hgSessionData) into an empty #sessionApp container, and this file builds the UI - a
-// save-current-view card, a searchable/sortable DataTable of saved sessions with inline
-// Overwrite/Share/Edit/Delete, and an "Advanced" panel for loading and backup.
+// save-current-view card, a custom-track backup button, a searchable/sortable DataTable of saved
+// sessions with inline Overwrite/Share/Edit/Delete, and an "Advanced" panel split into a load
+// column and a save column.
 //
 // The inline table actions POST to small JSON endpoints in hgSession.c (hgS_doDeleteJson, etc.) and
 // update the table in place.  Navigation actions (load a session, load from URL/file, save to file,
 // reset) are ordinary form submits/links against the existing hgSession actions.
 //
 // Styling: shared house-style components in gbModern.css (.gbPill, .gbCard, .gbModal*, .gbTable,
 // .gbBanner, .gbSection), session-specific layout in hgSession.css.
 
 /* global $, hgSessionData, convertTitleTagsToMouseovers, titleTagToMouseover, addMouseover */
 /* global htmlEncode, commify, gbShowTimingDialog */
 
 // Cart action variables (must match the hgs* defines in hgSession.h; hgSessionPrefix is "hgS_").
 var SESS_ACT = {
     save:      'hgS_doSaveSessionJson',
     rename:    'hgS_doRenameSessionJson',
@@ -604,49 +605,62 @@
     var loadFile =
         '<div class="sessAdvItem"><span class="lab">Load settings from a file</span>' +
         '<form class="sessAdvRow" action="hgSession" method="POST" enctype="multipart/form-data">' + sid +
         '<input class="sessAdvInput" type="file" name="hgS_loadLocalFileName">' +
         '<button type="submit" class="gbPill" name="hgS_doLoadLocal" value="submit" ' +
         'title="Load browser settings from a session file on your computer">Load</button></form></div>';
     var saveFile =
         '<div class="sessAdvItem"><span class="lab">Save settings to a file</span>' +
         '<form class="sessAdvRow" action="hgSession" method="POST">' + sid +
         '<input class="sessAdvInput" type="text" name="hgS_saveLocalFileName" ' +
         'placeholder="File name (blank = show in browser)">' +
         '<label class="sessSaveCheck"><input type="checkbox" name="hgS_saveLocalFileCompress" ' +
         'value="gzip"> gzip</label>' +
         '<button type="submit" class="gbPill" name="hgS_doSaveLocal" value="submit" ' +
         'title="Download the current browser settings as a session file">Save</button></form></div>';
-    var backup =
-        '<div class="sessAdvItem"><span class="lab">Back up custom tracks</span>' +
-        '<form class="sessAdvRow" action="hgSession" method="POST">' + sid +
-        '<button type="submit" class="gbPill" name="hgS_showDownload_" value="Submit" ' +
-        'title="Download your custom tracks as a .tar.gz archive you can reload later">' +
-        'Back up custom tracks (.tar.gz)</button></form></div>';
     var other =
         '<div class="sessAdvItem"><span class="lab">Other</span>' +
         '<div class="sessAdvLinks">' +
         '<a href="' + sessEnc(C.resetUrl) + '" ' +
         'title="Reset all browser settings to their defaults">Reset the browser to defaults</a>' +
         '</div></div>';
 
+    // Two columns - what you load from, and what you save to - rather than one undifferentiated
+    // list; "Other" (reset) belongs to neither, so it spans both underneath.
     return '<div class="sessAdv">' +
         '<div class="sessAdvHead" id="sessAdvHead"><span class="caret">▸</span>' +
         '<span>Advanced — load another user’s session, load from a URL or file, ' +
         'save to a file, reset the browser</span></div>' +
         '<div class="sessAdvBody" id="sessAdvBody" style="display:none">' +
-        loadUser + loadUrl + loadFile + saveFile + backup + other + '</div></div>';
+        '<div class="sessAdvCol"><div class="sessAdvColHead">Load</div>' +
+        loadUser + loadUrl + loadFile + '</div>' +
+        '<div class="sessAdvCol"><div class="sessAdvColHead">Save</div>' + saveFile + '</div>' +
+        '<div class="sessAdvOther">' + other + '</div>' +
+        '</div></div>';
+}
+
+function sessBackupHtml(C) {
+    // Backing up custom tracks used to be buried in the Advanced panel; it now sits just under the
+    // "Overwrite now" bar so it is visible without opening Advanced.
+    if (!C.loggedIn) { return ''; }
+    var sid = '<input type="hidden" name="' + sessEnc(C.cartVar) + '" value="' + sessEnc(C.hgsid) + '">';
+    return '<div class="sessBackup">' +
+        '<span>Custom tracks aren’t part of a saved session link.</span>' +
+        '<form action="hgSession" method="POST">' + sid +
+        '<button type="submit" class="gbPill" name="hgS_showDownload_" value="Submit" ' +
+        'title="Download your custom tracks as a .tar.gz archive you can reload later">' +
+        'Back up custom tracks (.tar.gz)</button></form></div>';
 }
 
 // ---- build the whole page -----------------------------------------------
 
 function sessAccountHtml(C) {
     // The signed-in account line (Signed in as X · Sign out · Change password) now lives in the
     // top-right menu bar, so it is intentionally not rendered here.  Kept commented out so QA can
     // add it back if wanted:
     /*
     if (C.loggedIn) {
         var s = 'Signed in as <b>' + sessEnc(C.userName) + '</b>';
         if (C.logoutUrl) { s += ' · <a href="' + sessEnc(C.logoutUrl) + '">Sign out</a>'; }
         if (C.changePasswordUrl) {
             s += ' · <a href="' + sessEnc(C.changePasswordUrl) + '">Change password</a>';
         }
@@ -660,55 +674,62 @@
     return '';
 }
 
 function sessSaveCardHtml(C) {
     if (!C.loggedIn) { return ''; }
     // Assembly and position separated by a colon (e.g. "hg38: chr7:1-1,000"); assembly is already
     // the accession for hubs (trackHubSkipHubName on the server).
     var loc = '';
     if (C.db && C.position) { loc = sessEnc(C.db) + ': ' + sessEnc(C.position); }
     else if (C.db) { loc = sessEnc(C.db); }
     else if (C.position) { loc = sessEnc(C.position); }
     if (loc && C.trackCount) {
         loc += ', ' + sessNum(C.trackCount) + ' track' + (C.trackCount === 1 ? '' : 's') + ' shown';
     }
     var what = loc ? '<span class="sessSaveWhat">' + loc + '</span>' : '';
+    // Ghost text for the name field is a real generated name, not an explanation of the feature -
+    // the "Session name" label above the box already says what it is.
+    var randName = sessEnc(sessRandomShareName());
     return '<div class="sessSaveCard">' +
         '<div class="sessSaveHead"><span class="sessSaveTitle">' +
         'Save the current view as a stable session link</span>' + what + '</div>' +
         '<div class="sessSaveRow">' +
+        '<div class="sessSaveField"><span class="lab">Session name</span>' +
         '<input id="sessSaveName" class="sessSaveInput" type="text" maxlength="255" ' +
-        'placeholder="Session name — or leave empty to save with a randomly generated name">' +
+        'placeholder="' + randName + '"></div>' +
         '<label class="sessSaveCheck"><input type="checkbox" id="sessSavePrivate"> ' +
         'Only I can load it</label>' +
         '<button type="button" class="gbPill primary" id="sessSaveBtn" ' +
         'title="Save your current browser view as a named session">Save session</button>' +
         '</div>' +
+        '<div class="sessSaveField"><span class="lab">Description</span>' +
         '<input id="sessSaveDesc" class="sessSaveInput" type="text" maxlength="512" ' +
-        'placeholder="Description (optional) — shown on hover and in the Public Sessions gallery">' +
+        'placeholder="Optional — shown on hover and in the Public Sessions gallery"></div>' +
         '</div>';
 }
 
 function sessRecentHtml(recent) {
     // A quick shortcut to re-save the session the user most recently saved, keeping its name and
-    // description.  Hidden when there are no saved sessions.
+    // description.  Hidden when there are no saved sessions.  The button's own mouseover is easy to
+    // miss, so a visible (i) bubble carries the same text.
     if (!recent) { return ''; }
+    var tip = 'Overwrite this session with the currently active view; keeps the session name and ' +
+        'description identical';
     return '<div class="sessRecent">Most recently saved session: <b>' + sessEnc(recent.name) +
         '</b> <span class="sessRecentTime">(' + sessEnc(recent.lastUse) + ')</span> ' +
-        '<button type="button" class="gbPill" id="sessUpdateNow" ' +
-        'title="Overwrite this session with the currently active view; keeps the session name and ' +
-        'description identical">Update now</button></div>';
+        '<button type="button" class="gbPill" id="sessUpdateNow" title="' + tip + '">' +
+        'Overwrite now</button> <span class="sessInfo" title="' + tip + '">&#9432;</span></div>';
 }
 
 function sessScrollToTable() {
     // Arriving from another server's Server column or "Go to" button, on a #sessions link. The
     // browser cannot act on that anchor by itself: the table is built here, long after it has
     // given up looking for the element, so scroll to it once it exists.
     if (window.location.hash !== '#sessions') { return; }
     var el = document.getElementById('sessions');
     if (el) { el.scrollIntoView(); }
 }
 
 function sessMirrorLinks(mirrors) {
     // "genome-euro", or "genome-euro or genome-asia", each linking to that server's own hgSession.
     var links = mirrors.map(function(m) {
         return '<a href="' + sessEnc(m.url) + '" target="_blank" title="' + sessEnc(m.title) +
@@ -777,77 +798,78 @@
     if (!C.loggedIn) { return ''; }
     // id="sessions" is the anchor a link from one of our other servers arrives on, see
     // sessScrollToTable().
     return '<div class="gbSection" id="sessions">Your saved sessions</div>' +
         '<table id="sessionAppTable" class="gbTable" style="width:100%"></table>';
 }
 
 function sessionBuild() {
     sessData = hgSessionData;
     // When the page was loaded with &measureTiming=1 the C side attaches sessData.timing; time the
     // client-side render too so the dialog shows the full server+client picture.
     var tBuildStart = (sessData.timing && window.performance) ? performance.now() : 0;
     var C = sessData.config;
     var app = $('#sessionApp');
 
-    var intro = '<div class="sessIntro">' + sessAccountHtml(C) +
-        (sessAccountHtml(C) ? '<br>' : '') +
-        'A session is a stable link to a Genome Browser view that you can save, load later, share ' +
-        'or copy into a manuscript. See the ' +
+    var acctHtml = sessAccountHtml(C);
+    var intro = '<div class="sessIntro">' + (acctHtml ? '<p>' + acctHtml + '</p>' : '') +
+        '<p>A session is a stable link to a Genome Browser view that you can save, load later, ' +
+        'share or copy into a manuscript. See the ' +
         '<a href="' + sessEnc(C.helpUrl) + '" target="_blank">Sessions User’s Guide</a> and the ' +
-        '<a href="' + sessEnc(C.galleryUrl) + '" target="_blank">Session Gallery</a>.</div>';
+        '<a href="' + sessEnc(C.galleryUrl) + '" target="_blank">Session Gallery</a>.</p></div>';
 
     // Every row carries the server it lives on; at this point they are all this server's own.
     // The sessions merged in later from the other nodes get theirs in sessMergeMirrors().
     (sessData.sessions || []).forEach(function(s, i) {
         s.uid = 'L' + i;
         s.server = C.thisServer ? C.thisServer.label : '';
         s.serverTitle = C.thisServer ? C.thisServer.title : '';
         s.serverUrl = '';   // this is that server, so the Server cell here is text, not a link
     });
 
-    // The session most recently saved/overwritten (max lastUse), for the one-click "Update now".
-    // Only this server's own sessions: "Update now" saves through this server's cart.
+    // The session most recently saved/overwritten (max lastUse), for the one-click "Overwrite now".
+    // Only this server's own sessions: "Overwrite now" saves through this server's cart.
     var recent = null;
     (sessData.sessions || []).forEach(function(s) {
         if (s.remote) { return; }
         if (!recent || s.lastUseEpoch > recent.lastUseEpoch) { recent = s; }
     });
 
     app.html(
         intro +
         '<div id="sessMsg" class="sessMsg"></div>' +
         sessRecentHtml(recent) +
+        sessBackupHtml(C) +
         sessSaveCardHtml(C) +
         sessAdvancedHtml(C) +
         sessTableHtml(C)
     );
 
     // Save current view.  Enter in either the name or the description field saves.
     $('#sessSaveBtn').on('click', sessDoSave);
     $('#sessSaveName, #sessSaveDesc').on('keydown', function(ev) {
         if (ev.key === 'Enter') { ev.preventDefault(); sessDoSave(); }
     });
 
-    // One-click update of the most recently saved session.
+    // One-click overwrite of the most recently saved session.
     if (recent) {
         $('#sessUpdateNow').on('click', function() {
             sessConfirm({
-                title: 'Update session',
+                title: 'Overwrite session',
                 bodyHtml: 'Overwrite <b>' + sessEnc(recent.name) + '</b> with the view you are ' +
                     'looking at now? The session name and description stay the same.',
-                okLabel: 'Update now',
+                okLabel: 'Overwrite now',
                 onOk: function() { sessDoOverwrite(recent); }
             });
         });
     }
 
     // Advanced toggle.
     $('#sessAdvHead').on('click', function() {
         var body = document.getElementById('sessAdvBody');
         var open = body.style.display !== 'none';
         body.style.display = open ? 'none' : 'grid';
         $(this).find('.caret').html(open ? '▸' : '▾');
     });
 
     // Session table, then the sessions our other servers hold, merged in as they arrive.
     if (C.loggedIn) { sessBuildTable(); sessScrollToTable(); sessMergeMirrors(); }