905b9cb05eeaca7f2dcda42fc6abdb95a2d2da7f
max
  Wed Sep 9 05:29:28 2026 -0700
no captcha for a command-line CGI run, and version the detailsScript module URL

Two small fixes to things noticed while adding the scatterPlot plot type.

A CGI run from the command line got the Cloudflare Turnstile challenge page
instead of the output the caller asked for, which makes "./hgc db=hg38 g=x" -
the quickest way to see what a CGI emits - useless without a hand-made hg.conf.
There is no browser to solve a captcha in that situation. printCaptcha() now
returns early when cgiWasSpoofed(). That flag cannot be set from an HTTP
request: cgiFromCommandLine() returns early and leaves it FALSE whenever the
web server has set REQUEST_METHOD. Checked that a plain argument-style run is
now clean, that a run which fakes the web environment with QUERY_STRING still
gets the captcha, and that an HTTP request behaves exactly as the unmodified
binary does.

The detailsScript module was loaded from a hardcoded import('../js/hgc.X.js'),
bypassing webTimeStampedLinkToResource(), so it was the one script on the page
with no ?v=<mtime>. That is the mechanism that flushes a browser's cache when
the CGI version changes and that keeps a mirror from pairing an old static file
with new CGIs, and without it a cached module could be handed newer bedDetails
JSON than it was written for. Now built through the helper, which also fixes the
already-shipped histogram type. The helper errAborts on a missing file and the
plot type comes from a hub, so a plot type with no module installed falls back to
the plain path: a silent failed import as before, rather than one bad hub setting
taking down the whole details page.

refs #35415

diff --git src/hg/hgc/bigBedClick.c src/hg/hgc/bigBedClick.c
index fdac6c12423..0eb19d930e3 100644
--- src/hg/hgc/bigBedClick.c
+++ src/hg/hgc/bigBedClick.c
@@ -1,767 +1,790 @@
 /* Handle details pages for wiggle tracks. */
 
 /* Copyright (C) 2013 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "wiggle.h"
 #include "cart.h"
 #include "hgc.h"
 #include "hubConnect.h"
 #include "hCommon.h"
 #include "hgColors.h"
 #include "bigBed.h"
 #include "hui.h"
 #include "subText.h"
 #include "web.h"
 #include "chromAlias.h"
 #include "quickLift.h"
 #include "hgConfig.h"
 #include "jsHelper.h"
 #include "jsonParse.h"
 #include "jsonWrite.h"
 #include "net.h"
 #include "trackHub.h"
 
 static void bigGenePredLinks(char *track, char *item)
 /* output links to genePred driven sequence dumps */
 {
 printf("<H3>Links to sequence:</H3>\n");
 printf("<UL>\n");
 puts("<LI>\n");
 hgcAnchorSomewhere("htcTranslatedPredMRna", item, "translate", seqName);
 printf("Translated Protein</A> from genomic DNA\n");
 puts("</LI>\n");
 
 puts("<LI>\n");
 hgcAnchorSomewhere("htcGeneMrna", item, track, seqName);
 printf("Predicted mRNA</A> \n");
 puts("</LI>\n");
 
 puts("<LI>\n");
 hgcAnchorSomewhere("htcGeneInGenome", item, track, seqName);
 printf("Genomic Sequence</A> from assembly\n");
 puts("</LI>\n");
 printf("</UL>\n");
 }
 
 void printMismatchString(char *a, char *b) 
 /* given two strings of same length, print . for every match and for mismatches, the letter of b */
 {
 int i = 0;
 while (TRUE)
     {
     if (a[i]=='\0' || b[i]=='\0')
         break;
     if (a[i]==b[i])
         printf(".");
     else
         printf("%c", (b[i]));
     i++;
     }
 }
 
 static void extFieldMismatchCounts(char *val)
 /* crispr track: number of mismatches. A comma-sep string of integers */
 {
 printf("<tr><td>Number of potential off-targets</td>\n");
 printf("<td>\n");
 char *words[255];
 int wordCount = chopByChar(val, ',', words, ArraySize(words));
 int i;
 printf("<table style='border-style: hidden'><tr>\n");
 for (i=0; i<wordCount; i++)
     printf("<td style='border:1px solid #CCCCCC; font-weight: normal; width:auto'><b>%d mismatches:</b><br>%s off-targets</td>", i, words[i]);
 printf("</tr></table>\n");
 }
 
 static void extFieldCrisprOfftargets(char *val, struct slPair *extraFields)
 /* crispr track: locations of off-targets. A |-separated string of coords, including strand and 
  a score
  e.g. chr15;63615585-;71|chr16;8835640+;70 */
 {
 if (NULL == val)
     {
     printf("<br><table class='bedExtraTbl'>\n");
     printf("<tr><td>Potential Off-targets</td>\n");
     printf("<td>No Off-targets found for this guide</td></tr>\n");
     printf("</table>\n");
     return;
     }
 printf("<tr><td>Potential Off-targets</td>\n");
 
 printf("<td>\n");
 char *coords[65536];
 int coordCount = chopByChar(val, '|', coords, ArraySize(coords));
 int i;
 
 struct subText *subList = NULL;
 slSafeAddHead(&subList, subTextNew("ig:", "intergenic "));
 slSafeAddHead(&subList, subTextNew("ex:", "exon "));
 slSafeAddHead(&subList, subTextNew("in:", "intron "));
 slSafeAddHead(&subList, subTextNew("|", "-"));
 
 boolean hasDb = sqlDatabaseExists(database);
 boolean hasLocus = FALSE;
 struct sqlConnection *conn = NULL;
 if (hasDb)
     {
     conn = hAllocConn(database);
     hasLocus = sqlTableExists(conn, "locusName");
     }
 
 if (coordCount==0)
     puts("Too many off-targets found to display or no off-targets. Please use the Crispor.org link at the top of the page to show all off-targets.\n");
 else
     {
     printf("<table style='border-collapse:collapse; font-size:12px; table-layout:fixed'>\n");
     printf("<tr>\n"
            "<th style='width:26em'>Mismatched nucleotides</th>\n"
            "<th style='width:9em'>CFD Score</th>\n");
     if (hasLocus)
            printf("<th style='width:40em'>Locus</th>\n");
     printf("<th style='width:30em'>Position</th></tr>\n");
     }
 
     
 boolean collapsed = FALSE;
 for (i=0; i<coordCount; i++)
     {
     if (i>10)
         {
         collapsed = TRUE;
         printf("<tr class='crisprLinkHidden' style='display:none'>\n");
         }
     else
         printf("<tr>\n");
 
     // parse single coordinate string
     // chr15;63615585-;71 = chrom;startPosStrand;scoreAsInt
     char *parts[3];
     chopByChar(coords[i], ';', parts, 3);
     char* chrom = parts[0];
     char* posStrand = parts[1];
     char* scoreStr = parts[2];
 
     // get score and strand
     char strand = *(posStrand+strlen(posStrand)-1);
     int pos = atol(posStrand);
     int scoreInt = atoi(scoreStr);
     float score = (float)scoreInt/1000;
 
     // get the DNA sequence - this is slow! twoBit currently does not cache
     // if the input is not sorted and this list is sorted by off-target score (CFD)
     struct dnaSeq *seq = hDnaFromSeq(database, chrom, pos, pos+23, dnaUpper);
     if (strand=='-')
         reverseComplement(seq->dna, seq->size);
     char *guideSeq = (char*)slPairFindVal(extraFields, "guideSeq");
     // PAM = the last three chars of the off-target
     int seqLen = strlen(seq->dna);
     char *pam = seq->dna+(seqLen-3);
         
     // print sequence + PAM
     printf("<td><tt>");
     printMismatchString(guideSeq, seq->dna);
     printf("&nbsp;%s", pam);
     printf("</tt></td>\n");
 
     // print score of off-target
     printf("<td>%0.3f</td>", score);
 
     // print name of this locus
     if (hasLocus)
         {
         struct sqlResult *sr = hRangeQuery(conn, "locusName", chrom, pos, pos+23, NULL, 0);
         char **row;
         row = sqlNextRow(sr);
         if (row != NULL)
             {
             char *desc = row[4];
             char *descLong = subTextString(subList, desc);
             printf("<td>%s</td>", descLong);
             freeMem(descLong);
             }
         sqlFreeResult(&sr);
         }
     
     // print link to location
     printf("<td><a href='%s&db=%s&position=%s%%3A%d-%d'>%s:%d (%c)</a></td>\n", 
         hgTracksPathAndSettings(), database,
         chrom, pos+1, pos+23, chrom, pos+1, strand);
 
     printf("</tr>\n");
     }
 if (hasDb)
     hFreeConn(&conn);
 printf("<tr>\n");
 if (coordCount!=0)
     printf("</table>\n");
 if (collapsed)
     {
     printf("<p><a id='crisprShowAllLink' href='#'>"
         "Show all %d off-targets...</a>\n", coordCount);
     jsOnEventById("click", "crisprShowAllLink", "crisprShowAll(); return false;");
     // inline .js is bad style but why pollute our global .js files for such a rare
     // case? Maybe we should have a generic "collapsible" class, like bootstrap?
     jsInline(
 	"function crisprShowAll() {\n"
 	"    $('#crisprShowAllLink').hide();\n"
 	"    $('.crisprLinkHidden').show();\n"
 	"    return false;\n"
     	"}\n"
 	);
     }
 }
 
 static void detailsTabPrintSpecial(char *name, char *val, struct slPair *extraFields)
 /* some extra fields require special printing code, they all start with '_'  */
 {
 if (sameWord(name, "_mismatchCounts"))
     extFieldMismatchCounts(val);
 else if (sameWord(name, "_crisprOfftargets"))
     extFieldCrisprOfftargets(val, extraFields);
 }
 
 static int seekAndPrintTable(struct trackDb *tdb, char *detailsUrl, off_t offset, struct slPair *extraFields)
 /* seek to 0 at url, get headers, then seek to offset, read tab-sep fields and output 
  * (extraFields are needed for some special field handlers). Return the number of fields
  * successfully printed.  */
 {
 int printCount = 0;
 // open the URL and get the first line
 char *headerLine = readOneLineMaybeBgzip(detailsUrl, 0, 0);
 if (headerLine == NULL)
     {
     printf("Error: Could not open the URL referenced in detailsUrls, %s", detailsUrl);
     return printCount;
     }
 
 boolean skipEmptyFields = trackDbSettingOn(tdb, "skipEmptyFields");
 
 // get the headers
 char *headers[1024];
 int headerCount = chopTabs(headerLine, headers);
 
 // clone the headers
 int i;
 for (i=0; i<headerCount; i++)
     headers[i] = cloneString(headers[i]);
 
 // read a line
 char *detailsLine = readOneLineMaybeBgzip(detailsUrl, offset, 0);;
 if (!detailsLine || isEmpty(detailsLine))
     return printCount;
 char *fields[1024];
 int fieldCount = chopTabs(detailsLine, fields);
 
 if (fieldCount!=headerCount)
     {
     printf("Error encountered when reading %s:<br>", detailsUrl);
     printf("The header line of the tab-sep file has a different number of fields compared ");
     printf("with the line pointed to by offset %lld in the bigBed file.<br>", (long long int)offset);
     printf("Number of headers: %d", headerCount);
     printf("Number of fields at offset: %d", fieldCount);
     return printCount;
     }
 struct slName *tblFieldNames = NULL;
 struct hash *fieldsToEmbeddedTbl = hashNew(0);
 struct embeddedTbl *tblList = NULL;
 getExtraTableFields(tdb, &tblFieldNames, &tblList, fieldsToEmbeddedTbl);
 
 // print the table for all external extra fields 
 printf("<br><table class='bedExtraTbl'>\n");
 fieldCount = min(fieldCount, headerCount);
 struct embeddedTbl *userTbl = NULL;
 struct dyString *tableLabelsDy = dyStringNew(0);
 dyStringPrintf(tableLabelsDy, "var _jsonHgcLabels = [");
 for (i=0; i<fieldCount; i++)
     {
     char *name = headers[i];
     char *val  = fields[i];
 
     // skip this field if it's empty and "skipEmptyFields" option is set
     if (skipEmptyFields && isEmpty(val))
         continue;
 
     // skip an optional '#' on the first field name
     if (i == 0 && startsWith("#", name))
         name = skipBeyondDelimit(name, '#');
 
     if (startsWith("_", name) && !(startsWith("_json", name)) && !(startsWith("json", name)))
         detailsTabPrintSpecial(name, val, extraFields);
     else if (slNameInList(tblFieldNames, name))
         {
         userTbl = (struct embeddedTbl *)hashFindVal(fieldsToEmbeddedTbl, name);
         userTbl->encodedTbl = val;
         printEmbeddedTable(tdb, userTbl, tableLabelsDy);
         }
     else
         {
         // the field name and value come from the hub's bigBed when this is a hub track
         char *encName = hubEncode(tdb, name);
         printFieldLabelWithId(encName, encName);
         printf("<td>%s</td></tr>\n", hubEncode(tdb, val));
         }
     printCount++;
     }
 printf("</table>\n");
 dyStringPrintf(tableLabelsDy, "];\n");
 jsInline(dyStringCannibalize(&tableLabelsDy));
 return printCount;
 }
 
 struct slPair *parseDetailsTablUrls(struct trackDb *tdb)
 /* Parse detailsUrls setting string into an slPair list of {offset column name, fileOrUrl} */
 {
 char *detailsUrlsStr = trackDbSetting(tdb, "detailsUrls");
 if (!detailsUrlsStr)
     detailsUrlsStr = trackDbSetting(tdb, "detailsTabUrls");
 if (!detailsUrlsStr)
     return NULL;
 
 struct slPair *detailsUrls = slPairListFromString(detailsUrlsStr, TRUE);
 if (!detailsUrls)
     {
     printf("Problem when parsing trackDb setting detailsUrls<br>\n");
     printf("Expected: a space-separated key=val list, like 'fieldName1=URL1 fieldName2=URL2'<br>\n");
     printf("But got: '%s'<br>", detailsUrlsStr);
     return NULL;
     }
 struct slPair *pair;
 for (pair = detailsUrls;  pair != NULL;  pair = pair->next)
     pair->val = hReplaceGbdb(replaceChars(pair->val, "$db", database));
 
 return detailsUrls;
 }
 
 static int printAllExternalExtraFields(struct trackDb *tdb, struct slPair *extraFields)
 /* handle the "detailsUrls" trackDb setting:
  * For each field, print a separate html table with all field names and values
  * from the external tab-sep file. Return the number of fields we successfully printed  */
 {
 int printCount = 0;
 struct slPair *detailsUrls = parseDetailsTablUrls(tdb), *pair;
 for (pair = detailsUrls; pair != NULL; pair = pair->next)
     {
     char *fieldName = pair->name;
     char *detailsUrl = pair->val;
 
     // get extra bigBed field (=the offset) and seek to it
     void *p = slPairFindVal(extraFields, fieldName);
     if (p==NULL)
         {
         printf("Error when parsing trackDb detailsUrls statement:<br>\n");
         printf("Cannot find extra bigBed field with name %s\n", fieldName);
         return 0;
         }
     char *offsetStr = (char*)p;
 
     if (offsetStr==NULL || sameWord(offsetStr, "0"))
 	{
 	/* need to show the empty off-targets for crispr tracks */
 	if (startsWith("crispr", tdb->track))
 	    extFieldCrisprOfftargets(NULL, NULL);
         // empty or "0" value in bigBed means that the lookup should not be performed
         continue;
 	}
     off_t offset = atoll(offsetStr);
 
     printCount += seekAndPrintTable(tdb, detailsUrl, offset, extraFields);
     }
 slPairFreeValsAndList(&detailsUrls);
 return printCount;
 }
 
 static struct hash *detailsScriptGroupByPlotType(struct trackDb *tdb)
 /* Parse detailsScript.<plotType>.<fieldName> trackDb settings and return a hash
  * of plotType -> slPair list (fieldName -> jsonConfig).  Returns NULL if no settings found.
  * See also hgc.c detailsScriptFieldNames() which parses the same settings for field skipping. */
 {
 struct slName *settings = trackDbLocalSettingsWildMatch(tdb, DETAILS_SCRIPT_PREFIX);
 if (settings == NULL)
     return NULL;
 struct hash *plotTypeHash = hashNew(0);
 struct slName *setting;
 for (setting = settings; setting != NULL; setting = setting->next)
     {
     // Parse "detailsScript.<plotType>.<fieldName>"
     char *key = cloneString(setting->name);
     char *dot1 = strchr(key, '.');
     if (dot1 == NULL)
         continue;
     dot1++;
     char *dot2 = strchr(dot1, '.');
     if (dot2 == NULL)
         continue;
     *dot2 = '\0';
     char *plotType = dot1;
     if (!isSymbolString(plotType)) // plotTypes must be simple strings - no XSS injection from hub
         continue;
     char *fieldName = dot2 + 1;
     char *jsonConfig = trackDbSetting(tdb, setting->name);
 
     struct slPair *entry;
     AllocVar(entry);
     entry->name = cloneString(fieldName);
     entry->val = cloneString(jsonConfig);
     struct slPair *existing = hashFindVal(plotTypeHash, plotType);
     slAddTail(&existing, entry);
     if (hashLookup(plotTypeHash, plotType) == NULL)
         hashAdd(plotTypeHash, plotType, entry);
     else
         hashReplace(plotTypeHash, plotType, existing);
     }
 slFreeList(&settings);
 return plotTypeHash;
 }
 
 static void bigBedClick(char *fileName, struct trackDb *tdb,
                      char *item, int start, int end, int bedSize)
 /* Handle click in generic bigBed track. */
 {
 char *chrom = cartString(cart, "c");
 
 /* Open BigWig file and get interval list. */
 struct bbiFile *bbi =  bigBedFileOpenAlias(fileName, chromAliasFindAliases);
 struct lm *lm = lmInit(0);
 int ivStart = start, ivEnd = end;
 char *itemForUrl = item;
 if (start == end)
     {
     // item is an insertion; expand the search range from 0 bases to 2 so we catch it:
     ivStart = max(0, start-1);
     ivEnd++;
     }
 char *quickLiftFile = cloneString(trackDbSetting(tdb, "quickLiftUrl"));
 struct hash *chainHash = NULL;
 struct bigBedInterval *bbList = NULL;
 if (quickLiftFile)
     bbList = quickLiftGetIntervals(quickLiftFile, bbi, chrom, ivStart, ivEnd, &chainHash);
 else
     bbList = bigBedIntervalQuery(bbi, chrom, ivStart, ivEnd, 0, lm);
 
 /* Get bedSize if it's not already defined. */
 if (bedSize == 0)
     bedSize = bbi->definedFieldCount;
 
 /* A bigBed always has at least chrom, chromStart and chromEnd.  A smaller count
  * can only come from a bad type line, and the bedSize - 3 below would then run
  * off the front of restFields[]. */
 if (bedSize < 3)
     errAbort("Track %s declares 'type bigBed %d', but a bigBed has at least 3 fields.",
              tdb->track, bedSize);
 
 char *scoreFilter = cartOrTdbString(cart, tdb, "scoreFilter", NULL);
 int minScore = 0;
 if (scoreFilter)
     minScore = atoi(scoreFilter);
 
 /* Find particular item in list - matching start, and item if possible. */
 boolean found = FALSE;
 boolean firstTime = TRUE;
 struct bigBedInterval *bb;
 for (bb = bbList; bb != NULL; bb = bb->next)
     {
     if (bedSize > 3)
 	{
 	char *name = cloneFirstWordByDelimiterNoSkip(bb->rest, '\t');
 	boolean match = (isEmpty(name) && isEmpty(item)) || sameOk(name, item);
 	freez(&name);
 	if (!match)
 	    continue;
 	}
 
     int seq1Seq2Fields = 0;
     // check for seq1 and seq2 in columns 7+8 (eg, pairedTagAlign)
     boolean seq1Seq2 = sameOk(trackDbSetting(tdb, BASE_COLOR_USE_SEQUENCE), "seq1Seq2");
     if (seq1Seq2 && bedSize == 6)
 	seq1Seq2Fields = 2;
     char *fields[bedSize+seq1Seq2Fields];
     char startBuf[16], endBuf[16];
     char *rest = cloneString(bb->rest);
     char *restFields[256];
     int restCount = 0;
     int restBedFields = 0;
     char **extraFields = NULL;
     int extraFieldCount = 0;
     struct slPair *extraFieldPairs = NULL;
     if (isNotEmpty(rest))
         {
         restCount = chopTabs(rest, restFields);
         restBedFields = bedSize - 3;
         if (restCount > restBedFields)
             {
             extraFields = (restFields + restBedFields);
             extraFieldCount = restCount - restBedFields;
             extraFieldPairs = getExtraFields(tdb, extraFields, extraFieldCount);
             }
         }
     int bbFieldCount = bigBedIntervalToRow(bb, chrom, startBuf, endBuf, fields,
                                            bedSize+seq1Seq2Fields);
     if (bbFieldCount != bedSize+seq1Seq2Fields)
         {
         errAbort("Disagreement between trackDb field count (%d) and %s fieldCount (%d)",
 		bedSize, fileName, bbFieldCount);
 	}
     struct bed *bed = NULL;
     if (quickLiftFile)
         {
         if ((bed = quickLiftIntervalsToBed(bbi, chainHash, bb)) == NULL)
             continue;
         }
     else
         {
         bed = bedLoadN(fields, bedSize);
         }
     if ((bed == NULL) || (bedSize >= 6 && scoreFilter && bed->score < minScore))
         continue;
     if (!(bed->chromStart == start && bed->chromEnd == end))
 	continue;
 
     found = TRUE;
     if (firstTime)
 	{
 	printf("<BR>\n");
 	firstTime = FALSE;
 	}
 
     // if there are extra fields, load them up because we may want to use them in URL:
     itemForUrl = getIdInUrl(tdb, item);
     printCustomUrlWithFields(tdb, bed->name, bed->name, item == itemForUrl, extraFieldPairs);
     if (itemForUrl)
         printIframe(tdb, itemForUrl);
 
     bedPrintPos(bed, bedSize, tdb);
 
     // display seq1 and seq2
     if (seq1Seq2 && bedSize+seq1Seq2Fields == 8)
         printf("<table><tr><th>Sequence 1</th><th>Sequence 2</th></tr>"
             "<tr><td> %s </td><td> %s </td></tr></table>", fields[6], fields[7]);
     else if (restCount > 0)
         {
         if (restCount > restBedFields)
             {
             int printCount = extraFieldsPrint(tdb, NULL, extraFields, extraFieldCount);
             printCount += printAllExternalExtraFields(tdb, extraFieldPairs);
 
             if (printCount == 0)
                 {
                 int i;
                 char label[20];
                 safef(label, sizeof(label), "nonBedFieldsLabel");
                 printf("<B>%s&nbsp;</B>",
                        trackDbSettingOrDefault(tdb, label, "Non-BED fields:"));
                 for (i = restBedFields;  i < restCount;  i++)
                     printf("%s%s", (i > 0 ? "\t" : ""), restFields[i]);
                 printf("<BR>\n");
                 }
             }
         if (sameString(tdb->type, "bigGenePred"))
             bigGenePredLinks(tdb->track, item);
         if (startsWith("hprcDeletions", tdb->track) || startsWith("hprcInserts", tdb->track) || startsWith("hprcArr", tdb->track))
             {
             // the source field, which is the first item after the itemRgb will
             // have all the other chains
             // TODO: make this controlled by a trackDb setting
             char *oChainList[2048];
             int i, numChains = chopCommas(cloneString(restFields[6]), oChainList);
             char *oChain = NULL;
             struct dyString *ds = dyStringNew(0);
             dyStringPrintf(ds, "var chainVis = {");
             for (i = 0; i < numChains; i++)
                 {
                 oChain = oChainList[i];
                 char *cartVar = catTwoStrings("chainHprc", oChain);
                 char *chainVis = cartOptionalString(cart, cartVar);
                 if (chainVis == NULL)
                     {
                     cartVar = catTwoStrings(cartVar, "_sel");
                     chainVis = cartOptionalString(cart, cartVar);
                     // TODO: this is not getting the vis right, because _sel is not the
                     // same as a visibility
                     }
                 dyStringPrintf(ds, "\"%s\": \"%s\", ", oChain, chainVis != NULL ? hStringFromTv(hTvFromString(chainVis)) : "Hide");
                 }
             dyStringPrintf(ds, "};\n");
             jsInline(dyStringCannibalize(&ds));
             }
         }
     if (isCustomTrack(tdb->track))
 	{
 	time_t timep = bbiUpdateTime(bbi);
 	printBbiUpdateTime(&timep);
 	}
     char *motifPwmTable = trackDbSetting(tdb, "motifPwmTable");
     if (motifPwmTable)
         {
         struct dnaSeq *seq = hDnaFromSeq(database, bed->chrom, bed->chromStart, bed->chromEnd, dnaLower);
         if (bed->strand[0] == '-')
             reverseComplement(seq->dna, seq->size);
         struct dnaMotif *motif = loadDnaMotif(bed->name, motifPwmTable);
         motifHitSection(seq, motif);
         }
 
     // detailsScript.*: load JS visualization scripts and export field data as JSON
     // see also hgc.c detailsScriptFieldNames() which parses the same settings to skip fields
     struct hash *plotTypeHash = detailsScriptGroupByPlotType(tdb);
     if (plotTypeHash)
         {
         // Build the bedDetails JSON object using jsonWrite
         struct jsonWrite *jw = jsonWriteNew();
         jsonWriteObjectStart(jw, NULL);
         jsonWriteString(jw, "track", tdb->track);
         jsonWriteString(jw, "chrom", chrom);
         jsonWriteNumber(jw, "start", bed->chromStart);
         jsonWriteNumber(jw, "end", bed->chromEnd);
         // Caching turned off for this session (the hgHubConnect file-caching button).
         // A module that fetches a file has to say so, because a GET the browser has
         // already cached would defeat it; the same flag hgTrackUi hands its own JS.
         if (isNotEmpty(cartOptionalString(cart, "udcTimeout")))
             jsonWriteBoolean(jw, "udcTimeout", TRUE);
         jsonWriteObjectStart(jw, "scripts");
 
         struct hashEl *hel, *helList = hashElListHash(plotTypeHash);
         for (hel = helList; hel != NULL; hel = hel->next)
             {
             struct slPair *fieldList = hel->val;
             jsonWriteListStart(jw, hel->name);
             struct slPair *fp;
             for (fp = fieldList; fp != NULL; fp = fp->next)
                 {
                 jsonWriteObjectStart(jw, NULL);
                 jsonWriteString(jw, "field", fp->name);
                 // Look up field value from bigBed extra fields
                 char *fv = "";
                 if (extraFieldPairs)
                     {
                     char *found = slPairFindVal(extraFieldPairs, fp->name);
                     if (found)
                         fv = found;
                     }
                 jsonWriteString(jw, "value", fv);
                 // Parse trackDb JSON config and merge its keys into this object
                 char *jsonConfig = fp->val;
                 if (isNotEmpty(jsonConfig))
                     {
                     struct jsonElement *configEl = jsonParse(jsonConfig);
                     // jsonObjectVal hands back NULL for a JSON null, and the hash
                     // routines below dereference their argument, so a hub writing
                     // "detailsScript.<plotType>.<field> null" would crash us.
                     struct hash *configHash = jsonObjectVal(configEl, "detailsScript config");
                     if (configHash == NULL)
                         {
                         jsonWriteObjectEnd(jw);
                         continue;
                         }
                     struct hashEl *cel, *celList = hashElListHash(configHash);
                     for (cel = celList; cel != NULL; cel = cel->next)
                         {
                         // A config key ending in "Url" names a file, by the same convention
                         // trackSettingIsFile() uses. The JS does not fetch it directly: it
                         // asks hgTrackUi for it, which checks the path against the hubs on
                         // this cart and reads it with udc. So resolve a relative path here
                         // against the track's own bigDataUrl, which works whether the hub
                         // was loaded over http or from a local path. A path the author
                         // already made absolute is left alone.
                         struct jsonElement *cval = cel->val;
                         if (endsWith(cel->name, "Url") && cval != NULL
                             && cval->type == jsonString && isNotEmpty(cval->val.jeString)
                             && !hasProtocol(cval->val.jeString)
                             && cval->val.jeString[0] != '/')
                             {
                             char *base = trackDbSetting(tdb, "bigDataUrl");
                             if (isNotEmpty(base))
                                 {
                                 char *abs = trackHubRelativeUrl(base, cval->val.jeString);
                                 if (abs != NULL)
                                     {
                                     jsonWriteString(jw, cel->name, abs);
                                     freeMem(abs);
                                     continue;
                                     }
                                 }
                             }
                         jsonWriteJsonElement(jw, cel->name, cval);
                         }
                     hashElFreeList(&celList);
 
                     // exportFields names other bigBed fields whose values are exported too,
                     // so one setting can drive a plot that needs several fields. Only fields
                     // that exist in this bigBed are exported, so a hub cannot name anything
                     // else, and the type is checked rather than asserted because jsonListVal
                     // and jsonStringVal errAbort on a mismatch and this JSON is hub-authored.
                     struct jsonElement *efEl = hashFindVal(configHash,
                                                            DETAILS_SCRIPT_EXPORT_FIELDS);
                     if (efEl != NULL && efEl->type == jsonList)
                         {
                         jsonWriteObjectStart(jw, "fieldValues");
                         struct slRef *ref;
                         int efCount = 0;
                         for (ref = efEl->val.jeList;
                              ref != NULL && efCount < DETAILS_SCRIPT_MAX_EXPORT;
                              ref = ref->next)
                             {
                             struct jsonElement *nameEl = ref->val;
                             if (nameEl == NULL || nameEl->type != jsonString)
                                 continue;
                             char *efName = nameEl->val.jeString;
                             if (isEmpty(efName) || extraFieldPairs == NULL)
                                 continue;
                             char *efVal = slPairFindVal(extraFieldPairs, efName);
                             if (efVal == NULL)
                                 continue;
                             jsonWriteString(jw, efName, efVal);
                             efCount++;
                             }
                         jsonWriteObjectEnd(jw);
                         }
                     }
                 jsonWriteObjectEnd(jw);
                 }
             jsonWriteListEnd(jw);
             }
 
         jsonWriteObjectEnd(jw);  // scripts
         jsonWriteObjectEnd(jw);  // root
 
         // Emit as inline JavaScript
         struct dyString *ds = dyStringNew(1024);
         dyStringPrintf(ds, "var bedDetails = %s;\n", jw->dy->string);
 
-        // Dynamically import and call each plot type's module
+        // Dynamically import and call each plot type's module. The URL carries
+        // ?v=<mtime>, as every other js file does, so that a browser cannot serve a
+        // cached module against newer bedDetails JSON and a mirror cannot pair an old
+        // module with new CGIs. webTimeStampedLinkToResource() errAborts on a missing
+        // file and plotType comes from a hub, so a plotType with no module installed
+        // falls back to the plain path: that leaves a silent failed import as before,
+        // rather than taking the whole details page down over one bad hub setting.
         for (hel = helList; hel != NULL; hel = hel->next)
+            {
+            char modFile[PATH_LEN];
+            safef(modFile, sizeof modFile, "hgc.%s.js", hel->name);
+            char fallBack[PATH_LEN];
+            safef(fallBack, sizeof fallBack, "../js/%s", modFile);
+            char *modUrl = fallBack;
+            char *docRoot = hDocumentRoot();
+            if (docRoot != NULL)
+                {
+                char onDisk[PATH_LEN];
+                safef(onDisk, sizeof onDisk, "%s/js/%s", docRoot, modFile);
+                if (fileExists(onDisk))
+                    modUrl = webTimeStampedLinkToResource(modFile, FALSE);
+                }
             dyStringPrintf(ds, "$(document).ready(function() {\n"
-                "  import('../js/hgc.%s.js').then(function(mod) { mod.%s(bedDetails); });\n"
-                "});\n", hel->name, hel->name);
+                "  import('%s').then(function(mod) { mod.%s(bedDetails); });\n"
+                "});\n", modUrl, hel->name);
+            if (modUrl != fallBack)
+                freeMem(modUrl);
+            }
 
         jsInline(dyStringCannibalize(&ds));
         jsonWriteFree(&jw);
         hashElFreeList(&helList);
         hashFree(&plotTypeHash);
         }
     }
 if (!found)
     {
     printf("No item %s starting at %d\n", emptyForNull(item), start);
     }
 lmCleanup(&lm);
 bbiFileClose(&bbi);
 }
 
 void genericBigBedClick(struct sqlConnection *conn, struct trackDb *tdb,
                      char *item, int start, int end, int bedSize)
 /* Handle click in generic bigBed track. */
 {
 char *fileName = bbiNameFromSettingOrTable(tdb, conn, tdb->table);
 bigBedClick(fileName, tdb, item, start, end, bedSize);
 }
 
 void bigBedCustomClick(struct trackDb *tdb)
 /* Display details for BigWig custom tracks. */
 {
 char *fileName = trackDbSetting(tdb, "bigDataUrl");
 char *item = cartOptionalString(cart, "i");
 int start = cartInt(cart, "o");
 int end = cartInt(cart, "t");
 bigBedClick(fileName, tdb, item, start, end, 0);
 }