8f801f0b7d0b28d3adbc5f26fc7cb8976ac2830f
hiram
  Fri May 22 13:48:48 2026 -0700
add the featureBits measurements to the user notification email refs #31811

diff --git src/hg/utils/otto/userRequests/ottoRequestWatch.sh src/hg/utils/otto/userRequests/ottoRequestWatch.sh
index 78868ee3593..98a5fd13074 100755
--- src/hg/utils/otto/userRequests/ottoRequestWatch.sh
+++ src/hg/utils/otto/userRequests/ottoRequestWatch.sh
@@ -6,30 +6,31 @@
 # web-server service user).  Picks up requests that ottoRequest.py has
 # acknowledged (status=1) and drives them through alignment setup
 # and workflow monitoring.
 #
 # Phase 1: new requests needing alignment setup - status=1 AND buildDir=''
 #          run ottoRequestAlign.sh to set up and launch the workflow
 # Phase 2: in-progress requests needing workflow monitoring
 #          run workflowMonitor.sh to poll Galaxy and install results
 #   0 pending, 1 notified, 2 in progress, 3 galaxy done, 4 tracks complete,
 #   5 ready to push, 6 push is done, 7 problems,
 #      8 final notification has been sent == process is complete
 ### cron job entry:
 #9,20,31,42,53 * * * * ~/kent/src/hg/utils/otto/userRequests/ottoRequestWatch.sh
 
 set -eEu -o pipefail
+umask 002
 
 export scriptDir=$(cd "$(dirname "$0")" && pwd)
 
 ##############################################################################
 ### singleton lock - only one instance at a time
 ### Open lockPath on FD 9 for the lifetime of the shell, then take a
 ### non-blocking exclusive lock.  Kernel releases the lock on exit
 ### (normal, error, or kill -9), so no stale lock cleanup is needed.
 ### Exit 0 silently if another instance holds the lock so cron doesn't
 ### email on every overlapping tick.  PID is written to the file for
 ### information only see the holder via:
 ###   cat ottoRequestWatch.lock      (the PID)
 ###   lsof ottoRequestWatch.lock     (the locking process)
 ##############################################################################
 export lockPath="${scriptDir}/ottoRequestWatch.lock"
@@ -42,30 +43,69 @@
 # truncates via a separate FD; FD 9 keeps its position 0 from <>, so
 # the printf below starts writing at the beginning of the empty file.
 : >"${lockPath}"
 printf "%d\n" "$$" >&9
 ##############################################################################
 
 ##############################################################################
 ### errors - set error status in the table
 function setErrorStatus() {
   id="${1}"
   /cluster/bin/x86_64/hgsql -N -e \
       "UPDATE ottoRequest SET status=7 WHERE id=${id};" hgcentraltest
 }
 ##############################################################################
 
+##############################################################################
+### getFeatureBitsPct - get percentage coverage from featureBits file
+###   args: srcDb dstDb buildDir
+###   returns percentage string (e.g., "45.2%") or empty string if not found
+###   mimics the featureBitsPct() function from ottoRequestView.cgi
+##############################################################################
+function getFeatureBitsPct() {
+  local srcDb="${1}"
+  local dstDb="${2}"
+  local buildDir="${3}"
+  local DstDb="${dstDb^}"  # first letter capitalized
+
+  if [[ -z "${srcDb}" || -z "${dstDb}" || -z "${buildDir}" ]]; then
+    return
+  fi
+
+  # determine subdirectory: trackData for GenArk, bed for UCSC native
+  local sub
+  if [[ "${srcDb}" == GC* ]]; then
+    sub="trackData"
+  else
+    sub="bed"
+  fi
+
+  # construct path to featureBits file
+#  local fbFile="${buildDir}/${sub}/lastz.${dstDb}/fb.${srcDb}.chain${DstDb}Link.txt"
+  local fbFile="${buildDir}/fb.${srcDb}.chain${DstDb}Link.txt"
+
+  if [[ -f "${fbFile}" ]]; then
+    # extract percentage from file using grep + sed (matches Python regex r'\(([\d.]+)%\)')
+    local pct
+    pct="$(grep -oE '\([0-9.]+%\)' "${fbFile}" 2>/dev/null | sed 's/[()]//g' | head -1)"
+    if [[ -n "${pct}" ]]; then
+      printf "%s" "${pct}"
+    fi
+  fi
+}
+##############################################################################
+
 ##############################################################################
 ### liftOverUrl - build the public download URL for an over.chain.gz file
 ###   args: srcDb dstDb
 ###   GenArk:      https://hgdownload.soe.ucsc.edu/hubs/<3>/<3>/<3>/<3>/<acc>/liftOver/<srcDb>To<DstDb>.over.chain.gz
 ###   UCSC native: https://hgdownload.soe.ucsc.edu/goldenPath/<srcDb>/liftOver/<srcDb>To<DstDb>.over.chain.gz
 ###   DstDb is dstDb with the first letter upper-cased (matches the
 ###   filename convention used in installLinks()).
 ###   verifies the URL with a curl HEAD before printing; returns 1 if
 ###   the URL does not resolve to a 2xx response.
 ##############################################################################
 function liftOverUrl() {
   local srcDb="${1}"
   local dstDb="${2}"
   local DstDb="${dstDb^}"
   local fileName="${srcDb}To${DstDb}.over.chain.gz"
@@ -464,36 +504,70 @@
   if [ -s "${buildDir}/successInvocationId.txt" ]; then
     invocationId=$(cut -f2 "${buildDir}/successInvocationId.txt")
     if ! "${scriptDir}/galaxyCleanup.py" "${profileJson}" "${invocationId}"; then
       printf "# WARNING: galaxy cleanup failed for request %s\n" "${reqId}" 1>&2
     fi
   fi
 
   if ! fromUrl="$(liftOverUrl "${fromDb}" "${toDb}")"; then
     setErrorStatus "${reqId}"
     continue
   fi
   if ! toUrl="$(liftOverUrl "${toDb}" "${fromDb}")"; then
     setErrorStatus "${reqId}"
     continue
   fi
+  # source the kegAlign.sh variables to get targetDb, queryDb, and swapDir
+  targetDb="" queryDb="" swapDir=""
+  if [[ -f "${buildDir}/kegAlign.sh" ]]; then
+    source <(grep -E '^export (swapDir|targetDb|queryDb)=' "${buildDir}/kegAlign.sh" 2>/dev/null || true)
+  fi
+  # get featureBits coverage percentages for both directions
+  # determine which direction is which and use appropriate build directory
+  fromToPct="" toFromPct=""
+  if [[ -n "${targetDb}" && -n "${queryDb}" && -n "${swapDir}" ]]; then
+    if [[ "${fromDb}" == "${targetDb}" ]]; then
+      # fromDb -> toDb uses buildDir, toDb -> fromDb uses swapDir
+      fromToPct="$(getFeatureBitsPct "${fromDb}" "${toDb}" "${buildDir}")"
+      toFromPct="$(getFeatureBitsPct "${toDb}" "${fromDb}" "${swapDir}")"
+    else
+      # fromDb -> toDb uses swapDir, toDb -> fromDb uses buildDir
+      fromToPct="$(getFeatureBitsPct "${fromDb}" "${toDb}" "${swapDir}")"
+      toFromPct="$(getFeatureBitsPct "${toDb}" "${fromDb}" "${buildDir}")"
+    fi
+  else
+    # fallback: try both directions with buildDir only (may not find swap direction)
+    fromToPct="$(getFeatureBitsPct "${fromDb}" "${toDb}" "${buildDir}")"
+    toFromPct="$(getFeatureBitsPct "${toDb}" "${fromDb}" "${buildDir}")"
+  fi
+
+  # construct coverage info for email
+  coverageInfo=""
+  if [[ -n "${fromToPct}" || -n "${toFromPct}" ]]; then
+    coverageInfo="
+Chain coverage (% of genome covered by alignments):
+  ${fromDb} -> ${toDb}: ${fromToPct:-"not available"}
+  ${toDb} -> ${fromDb}: ${toFromPct:-"not available"}
+"
+  fi
+
   sendNotification "${reqId}" \
 "from UCSC: liftOverRequest complete: ${fromDb}<->${toDb}" \
 "Your lift over request is complete:
      From:     ${fromDb}
        To:     ${toDb}
   comment:     ${comment}
 submitted:     ${requestTime}
-
+${coverageInfo}
 The lift.over files are available at these links:
 
   ${fromUrl}
   ${toUrl}
 "
 
   /cluster/bin/x86_64/hgsql -N -e \
       "UPDATE ottoRequest SET status=8, completeTime=now() WHERE id=${reqId};" hgcentraltest
 
 done < <(/cluster/bin/x86_64/hgsql -N -B -e \
   "SELECT id, fromDb, toDb, comment, requestTime, buildDir FROM ottoRequest \
    WHERE status = 6 AND requestType = 'liftOver';" hgcentraltest)