7f2c71980d90f22204d7aa2ca39cc09eaaf47d97
jcasper
  Fri Jun 3 19:20:38 2016 -0700
Added session details button, plus code review changes refs #15312, #17439

diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index 7ebaff2..7d7facf 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -13,30 +13,31 @@
 #include "hCommon.h"
 #include "hui.h"
 #include "cart.h"
 #include "jsHelper.h"
 #include "web.h"
 #include "hdb.h"
 #include "ra.h"
 #include "wikiLink.h"
 #include "customTrack.h"
 #include "customFactory.h"
 #include "udc.h"
 #include "hgSession.h"
 #include "hubConnect.h"
 #include "hgConfig.h"
 #include "sessionThumbnail.h"
+#include "obscure.h"
 
 void usage()
 /* Explain usage and exit. */
 {
 errAbort(
   "hgSession - Interface with wiki login and do session saving/loading.\n"
   "usage:\n"
   "    hgSession <various CGI settings>\n"
   );
 }
 
 /* Global variables. */
 struct cart *cart;
 char *excludeVars[] = {"Submit", "submit", NULL};
 struct slName *existingSessionNames = NULL;
@@ -266,109 +267,131 @@
 return val;
 }
 
 void showExistingSessions(char *userName)
 /* Print out a table with buttons for sharing/unsharing/loading/deleting
  * previously saved sessions. */
 {
 struct sqlConnection *conn = hConnectCentral();
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char query[512];
 boolean foundAny = FALSE;
 char *encUserName = cgiEncodeFull(userName);
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
+/* DataTables configuration: only allow ordering on session name, creation date, and database.
+ * https://datatables.net/reference/option/columnDefs */
 printf ("<script type=\"text/javascript\">"
         "$(document).ready(function () {\n"
-        "    $('#sessionTable').DataTable({\"columnDefs\": [{\"orderable\":false, \"targets\":[0,3,4,5,6,7,8]}],\n"
+        "    $('#sessionTable').DataTable({\"columnDefs\": [{\"orderable\":false, \"targets\":[0,4,5,6,7,8]}],\n"
         "                                       \"order\":[1,'asc']\n"
         "                                 });\n"
         "} );\n"
         "</script>\n");
 
 printf("<H3>My Sessions</H3>\n");
 printf("<div style=\"max-width:1024px\">");
 printf("<table id=\"sessionTable\" class=\"display compact\" borderwidth=0>\n");
 printf("<thead><tr>");
-printf("<TH><TD><B>session name (click to edit description)</B></TD><TD><B>created on</B></TD>"
-       "<TD align=center><B>use this&nbsp;<BR>session&nbsp;</B></TD>"
+printf("<TH><TD><B>session name (click to load)</B></TD><TD><B>created on</B></TD><td><b>assembly</b></td>"
+       "<TD align=center><B>view/edit&nbsp;<BR>details&nbsp;</B></TD>"
        "<TD align=center><B>delete this&nbsp;<BR>session&nbsp;</B></TD><TD align=center><B>share with&nbsp;<BR>others?&nbsp;</B></TD>"
        "<td align-center><b>post in&nbsp;<br><a href=\"../cgi-bin/hgPublicSessions?%s\">public listing</a>?</b></td>"
-       "<TD align=center><B>link to<BR>session</B></TD><TD align=center><B>send to<BR>mail</B></TD></TH>",
+       "<TD align=center><B>send to<BR>mail</B></TD></TH>",
        cartSidUrlString(cart));
 printf("</tr></thead>");
 printf("<tbody>\n");
 
 if (gotSettings)
-    sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, settings from %s "
+    sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, contents, settings from %s "
         "WHERE userName = '%s' ORDER BY sessionName;",
         namedSessionTable, encUserName);
 else
-    sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse from %s "
+    sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, contents from %s "
         "WHERE userName = '%s' ORDER BY sessionName;",
         namedSessionTable, encUserName);
 sr = sqlGetResult(conn, query);
 
 while ((row = sqlNextRow(sr)) != NULL)
     {
     char *encSessionName = row[0];
     char *sessionName = cgiDecodeClone(encSessionName);
     char *link = NULL;
     int shared = atoi(row[1]);
     char *firstUse = row[2];
     char buf[512];
     boolean inGallery = FALSE;
 
     if (shared >=2)
         inGallery = TRUE;
 
     printf("<TR><TD>&nbsp;&nbsp;</TD><TD>");
-    if (gotSettings)
-	    printf("<A HREF=\"%s?%s&%s=%s\">",
-	       hgSessionName(), cartSidUrlString(cart), hgsDoSessionDetail, encSessionName);
-    htmlTextOut(sessionName);
-    if (gotSettings)
-	    printf("</A>");
+
+    struct dyString *dy = dyStringNew(1024);
+    addSessionLink(dy, encUserName, encSessionName, FALSE);
+    printf("<a href=\"%s\">%s</a>", dyStringContents(dy), sessionName);
+    dyStringFree(&dy);
 
     char *spacePt = strchr(firstUse, ' ');
     if (spacePt != NULL) *spacePt = '\0';
         printf("&nbsp;&nbsp;</TD>"
 	        "<TD><nobr>%s<nobr>&nbsp;&nbsp;</TD><TD align=center>", firstUse);
 
-    safef(buf, sizeof(buf), "%s%s", hgsLoadPrefix, encSessionName);
-    cgiMakeButton(buf, "use");
+    char *dbIdx = NULL;
+    if (startsWith("db=", row[3]))
+        dbIdx = row[3]+3;
+    else
+        dbIdx = strstr(row[3], "&db=") + 4;
+        
+    if (dbIdx != NULL)
+        {
+        char *dbEnd = strchr(dbIdx, '&');
+        char *db = NULL;
+        if (dbEnd != NULL)
+            db = cloneStringZ(dbIdx, dbEnd-dbIdx);
+        else
+            db = cloneString(dbIdx);
+        printf("%s</td><td align=center>", db);
+        }
+    else
+        printf("n/a</td><td align=center>");
+
+    if (gotSettings)
+        {
+        safef(buf, sizeof(buf), "%s%s", hgsEditPrefix, encSessionName);
+        cgiMakeButton(buf, "details");
+        }
+    else
+        printf("unavailable");
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsDeletePrefix, encSessionName);
     char command[512];
     safef(command, sizeof(command), confirmDeleteFormat, sessionName);
     cgiMakeOnClickSubmitButton(command, buf, "delete");
 
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsSharePrefix, encSessionName);
     cgiMakeCheckBoxJS(buf, shared>0, "onchange=\"document.mainForm.submit();\"");
 
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsGalleryPrefix, encSessionName);
     cgiMakeCheckBoxFourWay(buf, inGallery, shared>0, NULL, NULL,
         "onchange=\"document.mainForm.submit();\"");
 
-    link = getSessionLink(encUserName, encSessionName);
-    printf("</TD><TD align=center>%s</TD>\n", link);
-    freez(&link);
     link = getSessionEmailLink(encUserName, encSessionName);
-    printf("<TD align=center>%s</TD></TR>", link);
+    printf("</td><td align=center>%s</td></tr>", link);
     freez(&link);
     foundAny = TRUE;
     struct slName *sn = slNameNew(sessionName);
     slAddHead(&existingSessionNames, sn);
     }
 if (!foundAny)
     printf("<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>(none)</TD>"
 	   "<TD colspan=5></TD></TR>\n");
 
 printf("</tbody>\n");
 
 printf("</TABLE>\n");
 printf("</div>\n");
 printf("<P></P>\n");
 sqlFreeResult(&sr);
@@ -638,30 +661,31 @@
 cartWebEnd();
 }
 
 
 void cleanHgSessionFromCart(struct cart *cart)
 /* Remove hgSession action variables that should not stay in the cart. */
 {
 char varName[256];
 safef(varName, sizeof(varName), "%s%s", cgiBooleanShadowPrefix(), hgsSharePrefix);
 cartRemovePrefix(cart, varName);
 cartRemovePrefix(cart, hgsSharePrefix);
 safef(varName, sizeof(varName), "%s%s", cgiBooleanShadowPrefix(), hgsGalleryPrefix);
 cartRemovePrefix(cart, varName);
 cartRemovePrefix(cart, hgsGalleryPrefix);
 cartRemovePrefix(cart, hgsLoadPrefix);
+cartRemovePrefix(cart, hgsEditPrefix);
 cartRemovePrefix(cart, hgsLoadLocalFileName);
 cartRemovePrefix(cart, hgsDeletePrefix);
 cartRemovePrefix(cart, hgsDo);
 cartRemove(cart, hgsOldSessionName);
 cartRemove(cart, hgsCancel);
 }
 
 static void outIfNotPresent(struct cart *cart, struct dyString *dy, char *track, int tdbVis)
 /* Output default trackDb visibility if it's not mentioned in the cart. */
 {
 char *cartVis = cartOptionalString(cart, track);
 if (cartVis == NULL)
     {
     if (dy)
         dyStringPrintf(dy,"&%s=%s", track, hStringFromTv(tdbVis));
@@ -784,31 +808,31 @@
 	  htmlEncode(sessionName), (shareSession ? "may" : "may not"),
 	  getSessionLink(encUserName, encSessionName),
 	  getSessionEmailLink(encUserName, encSessionName));
     cartCheckForCustomTracks(cart, dyMessage);
     }
 else
     dyStringPrintf(dyMessage,
 	  "Sorry, required table %s does not exist yet in the central "
 	  "database (%s).  Please ask a developer to create it using "
 	  "kent/src/hg/lib/namedSessionDb.sql .",
 	  namedSessionTable, sqlGetDatabase(conn));
 hDisconnectCentral(&conn);
 return dyStringCannibalize(&dyMessage);
 }
 
-void doGalleryAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn)
+void thumbnailAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn)
 /* Create a thumbnail image for the gallery.  Leaks memory from a generated filename string,
  * plus a couple of dyStrings.  Returns without determining if the image creation
  * succeeded. */
 {
 char query[4096];
 char **row;
 struct sqlResult *sr;
 sqlSafef(query, sizeof(query),
     "select m.idx, n.firstUse from gbMembers m join namedSessionDb n on m.userName = n.userName "
     "where m.userName = \"%s\" and n.sessionName = \"%s\"",
     encUserName, encSessionName);
 sr = sqlGetResult(conn, query);
 row = sqlNextRow(sr);
 if (row == NULL)
     errAbort("cannot add session to gallery; user %s, session %s",
@@ -819,52 +843,222 @@
     {
     struct dyString *hgTracksUrl = dyStringNew(0);
     addSessionLink(hgTracksUrl, encUserName, encSessionName, FALSE);
     struct dyString *renderUrl =
         dyStringSub(hgTracksUrl->string, "cgi-bin/hgTracks", "cgi-bin/hgRenderTracks");
     dyStringAppend(renderUrl, "&pix=640");
     char *renderCmd[] = {"wget", "-q", "-O", "-", renderUrl->string, NULL};
     char *convertCmd[] = {"convert", "-", "-resize", "320", "-crop", "320x240+0+0", destFile, NULL};
     char **cmdsImg[] = {renderCmd, convertCmd, NULL};
     pipelineOpen(cmdsImg, pipelineWrite, "/dev/null", NULL);
     }
 sqlFreeResult(&sr);
 }
 
 
-void doGalleryRemove(char *encUserName, char *encSessionName, struct sqlConnection *conn)
+void thumbnailRemove(char *encUserName, char *encSessionName, struct sqlConnection *conn)
 /* Unlink thumbnail image for the gallery.  Leaks memory from a generated filename string. */
 {
 char query[4096];
 char **row;
 struct sqlResult *sr;
 sqlSafef(query, sizeof(query),
     "select m.idx, n.firstUse from gbMembers m join namedSessionDb n on m.userName = n.userName "
     "where m.userName = \"%s\" and n.sessionName = \"%s\"",
     encUserName, encSessionName);
 sr = sqlGetResult(conn, query);
 row = sqlNextRow(sr);
 if (row == NULL)
     errAbort("cannot remove session from gallery; user %s, session %s",
         encUserName, encSessionName);
 
 char *filePath = sessionThumbnailFilePath(row[0], encSessionName, row[1]);
 if (filePath != NULL)
     unlink(filePath);
 sqlFreeResult(&sr);
 }
 
+char *doSessionDetail(char *userName, char *sessionName)
+/* Show details about a particular session. */
+{
+if (userName == NULL)
+    return "Sorry, please log in again.";
+struct dyString *dyMessage = dyStringNew(4096);
+char *encSessionName = cgiEncodeFull(sessionName);
+char *encUserName = cgiEncodeFull(userName);
+struct sqlConnection *conn = hConnectCentral();
+struct sqlResult *sr = NULL;
+char **row = NULL;
+char query[512];
+webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
+boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
+
+if (gotSettings)
+    sqlSafef(query, sizeof(query), "SELECT shared, firstUse, contents, settings from %s "
+	  "WHERE userName = '%s' AND sessionName = '%s'",
+          namedSessionTable, encUserName, encSessionName);
+else
+    sqlSafef(query, sizeof(query), "SELECT shared, firstUse contents from %s "
+	  "WHERE userName = '%s' AND sessionName = '%s'",
+          namedSessionTable, encUserName, encSessionName);
+sr = sqlGetResult(conn, query);
+if ((row = sqlNextRow(sr)) != NULL)
+    {
+    int shared = atoi(row[0]);
+    char *firstUse = row[1];
+    char *contents = row[2];
+    char *settings = NULL;
+    if (gotSettings)
+	settings = row[3];
+    char *description = getSetting(settings, "description");
+    if (description == NULL) description = "";
+
+    dyStringPrintf(dyMessage, "<A HREF=\"../goldenPath/help/hgSessionHelp.html#Details\" "
+		   "TARGET=_BLANK>Session Details Help</A><P/>\n");
+
+#define highlightAccChanges " var b = document.getElementById('" hgsDoSessionChange "'); " \
+                            "  if (b) { b.style.background = '#ff9999'; }"
+
+#define toggleGalleryDisable \
+                            "  var c = document.getElementById('detailsSharedCheckbox'); " \
+                            "  var d = document.getElementById('detailsGalleryCheckbox'); " \
+                            "  if (c.checked)" \
+                            "    {d.disabled = false;} " \
+                            "  else" \
+                            "    {d.disabled = true; " \
+                            "     d.checked = false; }"
+
+    dyStringPrintf(dyMessage, "<B>%s</B><P>\n"
+		   "<FORM ACTION=\"%s\" NAME=\"detailForm\" METHOD=GET>\n"
+		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=%s>"
+		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=\"%s\">"
+		   "Session Name: "
+		   "<INPUT TYPE=TEXT NAME=\"%s\" SIZE=%d VALUE=\"%s\" "
+		   "onChange=\"{%s}\" onKeypress=\"{%s}\">\n",
+		   sessionName, hgSessionName(),
+		   cartSessionVarName(cart), cartSessionId(cart), hgsOldSessionName, sessionName,
+		   hgsNewSessionName, 32, sessionName, highlightAccChanges, highlightAccChanges);
+    dyStringPrintf(dyMessage,
+		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"use\">"
+		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"delete\" "
+		   "onClick=\"" confirmDeleteFormat "\">"
+		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"accept changes\">"
+		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"cancel\"> "
+		   "<BR>\n",
+		   hgsLoadPrefix, encSessionName, hgsDeletePrefix, encSessionName,
+		   sessionName, hgsDoSessionChange, hgsDoSessionChange, hgsCancel);
+    dyStringPrintf(dyMessage,
+		   "Share with others? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
+		   "onChange=\"{%s %s}\" onClick=\"{%s %s}\" id=\"detailsSharedCheckbox\">\n"
+		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
+		   hgsSharePrefix, encSessionName, (shared>0 ? " CHECKED" : ""),
+		   highlightAccChanges, toggleGalleryDisable, highlightAccChanges, toggleGalleryDisable,
+		   cgiBooleanShadowPrefix(), hgsSharePrefix, encSessionName);
+
+    dyStringPrintf(dyMessage,
+		   "List in Public Sessions? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
+		   "onChange=\"{%s}\" onClick=\"{%s}\" id=\"detailsGalleryCheckbox\">\n"
+		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
+		   hgsGalleryPrefix, encSessionName, (shared>=2 ? " CHECKED" : ""),
+		   highlightAccChanges, highlightAccChanges,
+		   cgiBooleanShadowPrefix(), hgsGalleryPrefix, encSessionName);
+    /* Set initial disabled state of the gallery checkbox */
+    dyStringPrintf(dyMessage, "\n<script>\n%s\n</script>\n", toggleGalleryDisable);
+    dyStringPrintf(dyMessage,
+		   "Created on %s.<BR>\n", firstUse);
+    /* Print custom track counts per assembly */
+    if (startsWith("ctfile_", contents) || (strstr(contents, "&ctfile_") != NULL))
+        {
+        char *splitContents = cloneString(contents);
+        subChar(splitContents, '&', ' ');
+        struct hash *contentsHash = hashFromString(splitContents);
+        struct hash *ctValidCount = hashNew(2);
+        struct hash *ctInvalidCount = hashNew(2);
+        struct hashEl *elList = hashElListHash(contentsHash), *thisEl = elList;
+        while (thisEl != NULL)
+            {
+            if (startsWith("ctfile_", thisEl->name))
+                {
+                char *db = thisEl->name + 7;
+                char *ctFile = cloneString(thisEl->val);
+                cgiDecode(ctFile,ctFile,strlen(ctFile));
+                if (!fileExists(ctFile))
+                    {
+                    if (hashFindVal(ctInvalidCount, db) == NULL)
+                        hashAddInt(ctInvalidCount, db, 1);
+                    else
+                        hashIncInt(ctInvalidCount, db);
+                    }
+                else
+                    {
+                    if (hashFindVal(ctValidCount, db) == NULL)
+                        hashAddInt(ctValidCount, db, 1);
+                    else
+                        hashIncInt(ctValidCount, db);
+                    }
+                }
+                thisEl = thisEl->next;
+            }
+        elList = hashElListHash(ctValidCount);
+        if (elList != NULL)
+            {
+            dyStringPrintf(dyMessage, "Custom track counts per assembly: %s (%d)",
+                elList->name, ptToInt(elList->val));
+            thisEl = elList->next;
+            while (thisEl != NULL)
+                {
+                dyStringPrintf(dyMessage, ", %s (%d)", thisEl->name, ptToInt(thisEl->val));
+                thisEl = thisEl->next;
+                }
+            dyStringPrintf(dyMessage, "<br>\n");
+            }
+        elList = hashElListHash(ctInvalidCount);
+        if (elList != NULL)
+            {
+            dyStringPrintf(dyMessage, "Expired custom track counts per assembly: %s (%d)",
+                elList->name, ptToInt(elList->val));
+            thisEl = elList->next;
+            while (thisEl != NULL)
+                {
+                dyStringPrintf(dyMessage, ", %s (%d)", thisEl->name, ptToInt(thisEl->val));
+                thisEl = thisEl->next;
+                }
+            dyStringPrintf(dyMessage, "<br>\n");
+            }
+        }
+    if (gotSettings)
+        {
+        description = replaceChars(description, "\\\\", "\\__ESC__");
+        description = replaceChars(description, "\\r", "\r");
+        description = replaceChars(description, "\\n", "\n");
+        description = replaceChars(description, "\\__ESC__", "\\");
+        dyStringPrintf(dyMessage,
+            "Description:<BR>\n"
+            "<TEXTAREA NAME=\"%s\" ROWS=%d COLS=%d "
+            "onChange=\"%s\" onKeypress=\"%s\">%s</TEXTAREA><BR>\n",
+            hgsNewSessionDescription, 5, 80,
+            highlightAccChanges, highlightAccChanges, description);
+        }
+    dyStringAppend(dyMessage, "</FORM>\n");
+    sqlFreeResult(&sr);
+    }
+else
+    errAbort("doSessionDetail: got no results from query:<BR>\n%s\n", query);
+
+return dyStringCannibalize(&dyMessage);
+}
+
 char *doUpdateSessions(char *userName)
 /* Look for cart variables matching prefixes for sharing/unsharing,
  * loading or deleting a previously saved session.
  * Return a message confirming what we did, or NULL if no such variables
  * were in the cart. */
 {
 if (userName == NULL)
     return NULL;
 struct dyString *dyMessage = dyStringNew(1024);
 struct hashEl *cartHelList = NULL, *hel = NULL;
 struct sqlConnection *conn = hConnectCentral();
 char *encUserName = cgiEncodeFull(userName);
 boolean didSomething = FALSE;
 char query[512];
 
@@ -888,33 +1082,33 @@
 	boolean inGallery = hashIntVal(galleryHash, encSessionName) >= 2 ? TRUE : FALSE;
 	boolean newGallery  = cartUsualInt(cart, hel->name, 0) > 0 ? TRUE : FALSE;
 
 	if (newGallery != inGallery)
 	    {
 	    sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d "
 		  "WHERE userName = '%s' AND sessionName = '%s';",
 		  namedSessionTable, newGallery == TRUE ? 2 : 1, encUserName, encSessionName);
 	    sqlUpdate(conn, query);
 	    sessionTouchLastUse(conn, encUserName, encSessionName);
 	    dyStringPrintf(dyMessage,
 			   "Marked session <B>%s</B> as %s.<BR>\n",
 			   htmlEncode(sessionName),
 			   (newGallery == TRUE ? "added to gallery" : "removed from public listing"));
         if (newGallery == FALSE)
-            doGalleryRemove(encUserName, encSessionName, conn);
+            thumbnailRemove(encUserName, encSessionName, conn);
         if (newGallery == TRUE)
-            doGalleryAdd(encUserName, encSessionName, conn);
+            thumbnailAdd(encUserName, encSessionName, conn);
 	    didSomething = TRUE;
 	    }
 	}
     hashFree(&galleryHash);
     }
 
 cartHelList = cartFindPrefix(cart, hgsSharePrefix);
 if (cartHelList != NULL)
     {
     struct hash *sharedHash = hashNew(0);
     char **row;
     struct sqlResult *sr;
     sqlSafef(query, sizeof(query),
 	  "select sessionName,shared from %s where userName = '%s'",
 	  namedSessionTable, encUserName);
@@ -930,68 +1124,77 @@
     boolean inGallery = hashIntVal(sharedHash, encSessionName) >= 2 ? TRUE : FALSE;
 	boolean newShared = cartUsualInt(cart, hel->name, 1) ? TRUE : FALSE;
 
 	if (newShared != alreadyShared)
 	    {
 	    sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d "
 		  "WHERE userName = '%s' AND sessionName = '%s';",
 		  namedSessionTable, newShared, encUserName, encSessionName);
 	    sqlUpdate(conn, query);
 	    sessionTouchLastUse(conn, encUserName, encSessionName);
 	    dyStringPrintf(dyMessage,
 			   "Marked session <B>%s</B> as %s.<BR>\n",
 			   htmlEncode(sessionName),
 			   (newShared == TRUE ? "shared" : "unshared"));
         if (newShared == FALSE && inGallery == TRUE)
-            doGalleryRemove(encUserName, encSessionName, conn);
+            thumbnailRemove(encUserName, encSessionName, conn);
 	    didSomething = TRUE;
 	    }
 	}
     hashFree(&sharedHash);
     }
 
+hel = cartFindPrefix(cart, hgsEditPrefix);
+if (hel != NULL)
+    {
+    char *encSessionName = hel->name + strlen(hgsEditPrefix);
+    char *sessionName = cgiDecodeClone(encSessionName);
+    dyStringPrintf(dyMessage, "%s", doSessionDetail(userName, sessionName));
+    didSomething = TRUE;
+    }
+
 hel = cartFindPrefix(cart, hgsLoadPrefix);
 if (hel != NULL)
     {
     char *encSessionName = hel->name + strlen(hgsLoadPrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     char wildStr[256];
     safef(wildStr, sizeof(wildStr), "%s*", hgsLoadPrefix);
     dyStringPrintf(dyMessage,
 		   "Loaded settings from session <B>%s</B>. %s %s<BR>\n",
 		   htmlEncode(sessionName),
 		   getSessionLink(encUserName, encSessionName),
 		   getSessionEmailLink(encUserName, encSessionName));
     cartLoadUserSession(conn, userName, sessionName, cart, NULL, wildStr);
     cartHideDefaultTracks(cart);
     hubConnectLoadHubs(cart);
     cartCopyCustomTracks(cart);
     cartCheckForCustomTracks(cart, dyMessage);
     didSomething = TRUE;
     }
 
 cartHelList = cartFindPrefix(cart, hgsDeletePrefix);
 for (hel = cartHelList;  hel != NULL;  hel = hel->next)
     {
     char *encSessionName = hel->name + strlen(hgsDeletePrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     sqlSafef(query, sizeof(query), "select shared from %s "
       "where userName = '%s' and sessionName = '%s';",
       namedSessionTable, encUserName, encSessionName);
     int shared = sqlQuickNum(conn, query);
     if (shared >= 2)
-        doGalleryRemove(encUserName, encSessionName, conn);
+        thumbnailRemove(encUserName, encSessionName, conn);
     sqlSafef(query, sizeof(query), "DELETE FROM %s "
 	  "WHERE userName = '%s' AND sessionName = '%s';",
 	  namedSessionTable, encUserName, encSessionName);
     sqlUpdate(conn, query);
     dyStringPrintf(dyMessage,
 		   "Deleted session <B>%s</B>.<BR>\n",
 		   htmlEncode(sessionName));
     didSomething = TRUE;
     }
 
 hDisconnectCentral(&conn);
 if (didSomething)
     return(dyStringCannibalize(&dyMessage));
 else
     {
@@ -1117,140 +1320,30 @@
 	   cgiAppendSForHttps(), cgiServerNamePort(), destAppScriptName(),
 	   cartSessionVarName(), cartSessionId(cart));
     }
 if (lf != NULL)
     {
     cartLoadSettings(lf, cart, NULL, actionVar);
     cartHideDefaultTracks(cart);
     hubConnectLoadHubs(cart);
     cartCopyCustomTracks(cart);
     cartCheckForCustomTracks(cart, dyMessage);
     lineFileClose(&lf);
     }
 return dyStringCannibalize(&dyMessage);
 }
 
-char *doSessionDetail(char *userName, char *sessionName)
-/* Show details about a particular session. */
-{
-if (userName == NULL)
-    return "Sorry, please log in again.";
-struct dyString *dyMessage = dyStringNew(4096);
-char *encSessionName = cgiEncodeFull(sessionName);
-char *encUserName = cgiEncodeFull(userName);
-struct sqlConnection *conn = hConnectCentral();
-struct sqlResult *sr = NULL;
-char **row = NULL;
-char query[512];
-webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
-boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
-
-if (gotSettings)
-    sqlSafef(query, sizeof(query), "SELECT shared, firstUse, settings from %s "
-	  "WHERE userName = '%s' AND sessionName = '%s'",
-          namedSessionTable, encUserName, encSessionName);
-else
-    sqlSafef(query, sizeof(query), "SELECT shared, firstUse from %s "
-	  "WHERE userName = '%s' AND sessionName = '%s'",
-          namedSessionTable, encUserName, encSessionName);
-sr = sqlGetResult(conn, query);
-if ((row = sqlNextRow(sr)) != NULL)
-    {
-    int shared = atoi(row[0]);
-    char *firstUse = row[1];
-    char *settings = NULL;
-    if (gotSettings)
-	settings = row[2];
-    char *description = getSetting(settings, "description");
-    if (description == NULL) description = "";
-
-    dyStringPrintf(dyMessage, "<A HREF=\"../goldenPath/help/hgSessionHelp.html#Details\" "
-		   "TARGET=_BLANK>Session Details Help</A><P/>\n");
-
-#define highlightAccChanges " var b = document.getElementById('" hgsDoSessionChange "'); " \
-                            "  if (b) { b.style.background = '#ff9999'; }"
-
-#define toggleGalleryDisable \
-                            "  var c = document.getElementById('detailsSharedCheckbox'); " \
-                            "  var d = document.getElementById('detailsGalleryCheckbox'); " \
-                            "  if (c.checked)" \
-                            "    {d.disabled = false;} " \
-                            "  else" \
-                            "    {d.disabled = true; " \
-                            "     d.checked = false; }"
-
-    dyStringPrintf(dyMessage, "<B>%s</B><P>\n"
-		   "<FORM ACTION=\"%s\" NAME=\"detailForm\" METHOD=GET>\n"
-		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=%s>"
-		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=\"%s\">"
-		   "Session Name: "
-		   "<INPUT TYPE=TEXT NAME=\"%s\" SIZE=%d VALUE=\"%s\" "
-		   "onChange=\"{%s}\" onKeypress=\"{%s}\">\n",
-		   sessionName, hgSessionName(),
-		   cartSessionVarName(cart), cartSessionId(cart), hgsOldSessionName, sessionName,
-		   hgsNewSessionName, 32, sessionName, highlightAccChanges, highlightAccChanges);
-    dyStringPrintf(dyMessage,
-		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"use\">"
-		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"delete\" "
-		   "onClick=\"" confirmDeleteFormat "\">"
-		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"accept changes\">"
-		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"cancel\"> "
-		   "<BR>\n",
-		   hgsLoadPrefix, encSessionName, hgsDeletePrefix, encSessionName,
-		   sessionName, hgsDoSessionChange, hgsDoSessionChange, hgsCancel);
-    dyStringPrintf(dyMessage,
-		   "Share with others? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
-		   "onChange=\"{%s %s}\" onClick=\"{%s %s}\" id=\"detailsSharedCheckbox\">\n"
-		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
-		   hgsSharePrefix, encSessionName, (shared>0 ? " CHECKED" : ""),
-		   highlightAccChanges, toggleGalleryDisable, highlightAccChanges, toggleGalleryDisable,
-		   cgiBooleanShadowPrefix(), hgsSharePrefix, encSessionName);
-
-    dyStringPrintf(dyMessage,
-		   "List in Public Sessions? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
-		   "onChange=\"{%s}\" onClick=\"{%s}\" id=\"detailsGalleryCheckbox\">\n"
-		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
-		   hgsGalleryPrefix, encSessionName, (shared>=2 ? " CHECKED" : ""),
-		   highlightAccChanges, highlightAccChanges,
-		   cgiBooleanShadowPrefix(), hgsGalleryPrefix, encSessionName);
-
-    /* Set initial disabled state of the gallery checkbox */
-    dyStringPrintf(dyMessage, "\n<script>\n%s\n</script>\n", toggleGalleryDisable);
-    dyStringPrintf(dyMessage,
-		   "Created on %s.<BR>\n", firstUse);
-    if (gotSettings)
-	{
-	description = replaceChars(description, "\\\\", "\\__ESC__");
-	description = replaceChars(description, "\\r", "\r");
-	description = replaceChars(description, "\\n", "\n");
-	description = replaceChars(description, "\\__ESC__", "\\");
-	dyStringPrintf(dyMessage,
-		       "Description:<BR>\n"
-		       "<TEXTAREA NAME=\"%s\" ROWS=%d COLS=%d "
-		       "onChange=\"%s\" onKeypress=\"%s\">%s</TEXTAREA><BR>\n",
-		       hgsNewSessionDescription, 5, 80,
-		       highlightAccChanges, highlightAccChanges, description);
-	}
-    dyStringAppend(dyMessage, "</FORM>\n");
-    sqlFreeResult(&sr);
-    }
-else
-    errAbort("doSessionDetail: got no results from query:<BR>\n%s\n", query);
-
-return dyStringCannibalize(&dyMessage);
-}
-
 void renamePrefixedCartVar(char *prefix, char *oldName, char *newName)
 /* If cart has prefix+oldName, replace it with prefix+newName = submit. */
 {
 char varName[256];
 safef(varName, sizeof(varName), "%s%s", prefix, oldName);
 if (cartVarExists(cart, varName))
     {
     cartRemove(cart, varName);
     safef(varName, sizeof(varName), "%s%s", prefix, newName);
     cartSetString(cart, varName, "submit");
     }
 }
 
 char *doSessionChange(char *userName, char *oldSessionName)
 /* Process changes to session from session details page. */
@@ -1290,62 +1383,64 @@
 else
     errAbort("doSessionChange: got no results from query:<BR>\n%s\n", query);
 
 char *newName = cartOptionalString(cart, hgsNewSessionName);
 if (isNotEmpty(newName) && !sameString(sessionName, newName))
     {
     char *encNewName = cgiEncodeFull(newName);
     sqlSafef(query, sizeof(query),
 	  "UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';",
 	  namedSessionTable, encNewName, encUserName, encSessionName);
 	sqlUpdate(conn, query);
     dyStringPrintf(dyMessage, "Changed session name from %s to <B>%s</B>.\n",
 		   sessionName, newName);
     sessionName = newName;
     encSessionName = encNewName;
+    renamePrefixedCartVar(hgsEditPrefix, encOldSessionName, encNewName);
     renamePrefixedCartVar(hgsLoadPrefix, encOldSessionName, encNewName);
     renamePrefixedCartVar(hgsDeletePrefix, encOldSessionName, encNewName);
     if (shared >= 2)
         {
-        doGalleryRemove(encUserName, encSessionName, conn);
-        doGalleryAdd(encUserName, encNewName, conn);
+        thumbnailRemove(encUserName, encSessionName, conn);
+        thumbnailAdd(encUserName, encNewName, conn);
         }
     }
 
 char sharedVarName[256];
 char galleryVarName[256];
 safef(sharedVarName, sizeof(sharedVarName), hgsSharePrefix "%s", encOldSessionName);
 safef(galleryVarName, sizeof(galleryVarName), hgsGalleryPrefix "%s", encOldSessionName);
 if (cgiBooleanDefined(sharedVarName) || cgiBooleanDefined(galleryVarName))
     {
     int newShared = shared;
     if (cgiBooleanDefined(sharedVarName))
         newShared = cartBoolean(cart, sharedVarName) ? 1 : 0;
     if (cgiBooleanDefined(galleryVarName))
         newShared = cartBoolean(cart, galleryVarName) ? 2 : newShared;
     if (newShared != shared)
         {
         sqlSafef(query, sizeof(query),
             "UPDATE %s set shared = %d WHERE userName = '%s' AND sessionName = '%s';",
             namedSessionTable, newShared, encUserName, encSessionName);
         sqlUpdate(conn, query);
         dyStringPrintf(dyMessage, "Marked session <B>%s</B> as %s.<BR>\n",
-            htmlEncode(sessionName), (newShared>0 ? newShared>=2 ? "shared in public listing" : "shared, but not in public listing" : "unshared"));
+            htmlEncode(sessionName), (newShared>0 ? newShared>=2 ? "shared in public listing" :
+                "shared, but not in public listing" : "unshared"));
         if (shared >= 2 && newShared < 2)
-            doGalleryRemove(encUserName, encSessionName, conn);
+            thumbnailRemove(encUserName, encSessionName, conn);
         if (shared < 2 && newShared >= 2)
-            doGalleryAdd(encUserName, encSessionName, conn);
+            thumbnailAdd(encUserName, encSessionName, conn);
         }
     cartRemove(cart, sharedVarName);
     cartRemove(cart, galleryVarName);
     char shadowVarName[512];
     safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), sharedVarName);
     cartRemove(cart, shadowVarName);
     safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), galleryVarName);
     cartRemove(cart, shadowVarName);
     }
 if (gotSettings)
     {
     struct hash *settingsHash = raFromString(settings);
     char *description = hashFindVal(settingsHash, "description");
     char *newDescription = cartOptionalString(cart, hgsNewSessionDescription);
     if (newDescription != NULL)