Index: /trunk/ippMonitor/INSTALL
===================================================================
--- /trunk/ippMonitor/INSTALL	(revision 42971)
+++ /trunk/ippMonitor/INSTALL	(revision 42972)
@@ -45,5 +45,7 @@
 # czartool_plots -> /export/ipp113.0/ipp/czartool_plots/
 # --usePDO selects a modern PHP/mysql interface
-psconfigure --htdocs /var/www/localhost/htdocs --wwwbin /var/www/localhost/wwwbin --pltdir /var/www/localhost/htdocs/ippMonitor --site ~/ippconfig/ippmonitor.config --usePDO
+#psconfigure --htdocs /var/www/localhost/htdocs --wwwbin /var/www/localhost/wwwbin --pltdir /var/www/localhost/htdocs/ippMonitor --site ~/ippconfig/ippmonitor.config --usePDO
+# for ippm@grens1
+psconfigure --htdocs /var/www/html --wwwbin /var/www/html/wwwbin --pltdir /var/www/html/ippMonitor --site ~/ippconfig/ippmonitor.config --usePDO
 
 # note that modern php requires the use of PDO, the older MDB2 and DB mysql interfaces have been deprecated
Index: /trunk/ippMonitor/czartool/czarpoll.pl
===================================================================
--- /trunk/ippMonitor/czartool/czarpoll.pl	(revision 42971)
+++ /trunk/ippMonitor/czartool/czarpoll.pl	(revision 42972)
@@ -26,4 +26,12 @@
 use czartool::DayMetrics;
 use czartool::MetricsIndex;
+
+# --- Logging setup ---
+use IO::Handle;
+my $log_file = "/tmp/czarpoll.log";
+open(my $LOG, '>>', $log_file) or die "Cannot open log file $log_file: $!";
+$LOG->autoflush(1);
+select($LOG);  # Redirect all print statements to the log file
+open(STDERR, '>&', $LOG) or die "Cannot redirect STDERR to log file: $!";
 
 my $period = 60;
@@ -149,7 +157,9 @@
         my @labels = @{$pantasks->getLabels($server,'gpc1')};
 	push (@allLabels, @labels);
+        #print "[$server gpc1] labels: ", join(", ", @labels), "\n";
 
         my @labels2 = @{$pantasks->getLabels($server,'gpc2')};
 	push (@allLabels2, @labels2);
+        #print "[$server gpc2] labels: ", join(", ", @labels2), "\n";
     }
     #The way gpc2 pantasks are set up is different from gpc1. Therefore, the same label can appear in both stdscience and distribution. 
@@ -309,4 +319,15 @@
     while (1) {
 
+        # --- Truncate log file at start of each iteration ---
+        close($LOG);
+        open($LOG, '>', $log_file) or die "Cannot reopen log file $log_file: $!";
+        $LOG->autoflush(1);
+        select($LOG); # Redirect STDERR (error messages) to the same log file
+        open(STDERR, '>&', $LOG) or die "Cannot redirect STDERR to log file: $!";
+
+        print "\n======================== CZARPOLL ITERATION START ========================\n";
+        print scalar(localtime) . "\n";
+        print "==========================================================================\n";
+
         my $iterationStartTime = time();
  
@@ -447,4 +468,6 @@
 
         print "--------------------------------------------------------------------------\n";
+        print "Iteration complete at " . scalar(localtime) . "\n";
+        print "==========================================================================\n";
         if ($iterationTime < $period) {
             print "* Going to sleep\n";
Index: /trunk/ippMonitor/czartool/extra_labels.dat
===================================================================
--- /trunk/ippMonitor/czartool/extra_labels.dat	(revision 42971)
+++ /trunk/ippMonitor/czartool/extra_labels.dat	(revision 42972)
@@ -2,5 +2,5 @@
 ## Priorities for the labels must be added to the priorities mySQL table for the relevant telescope, if desired.
 ## PV3.Pole.Reprocess.20180510 gpc1
-ps_ud_QUB gpc1
+## ps_ud_QUB gpc1
 ## update.ipp138.fix gpc1
 ## update.ipp138.fix gpc2
Index: /trunk/ippMonitor/raw/czartool_labels.php
===================================================================
--- /trunk/ippMonitor/raw/czartool_labels.php	(revision 42971)
+++ /trunk/ippMonitor/raw/czartool_labels.php	(revision 42972)
@@ -25,55 +25,55 @@
 menu($myMenu, 'Czartool on '.$lastUpdateTime, 'ipp.css', $ID['link'], $ID['proj']);
 
-$stages=array(
-        "chip",
-        "cam",
-        "fake",
-        "warp",
-        "stack",
-        "staticsky",
-        "fullforce",
-        "diff",
-        "dist", 
-        "pub",
-	);
-
-$getFailures = array(
-        "chip"      => "failedChipProcessedImfile.php",
-        "cam"       => "failedCamProcessedExp.php",
-        "fake"      => "failedFakeProcessedImfile.php",
-        "warp"      => "failedWarpSkyfiles.php",
-        "stack"     => "failedStackSkyfile.php",
-        "staticsky" => "failedStaticsky.php",
-        "fullforce" => "failedFullForceSkyfiles.php",
-        "diff"      => "failedDiffSkyfile.php",
-        "dist"      => "NONE", 
-        "pub"       => "publishDone.php",
-);	     
-
-$mainTable = array(
-        "chip"      => "chipRun",
-        "cam"       => "camRun",
-        "fake"      => "fakeRun",
-        "warp"      => "warpRun",
-        "stack"     => "stackRun",
-        "staticsky" => "staticskyRun",
-        "fullforce" => "fullForceRun",
-        "diff"      => "diffRun",
-        "dist"      => "distRun", 
-        "pub"       => "publishRun",
-);	     
-
-$addSearchState = array(
-        "chip"      => 0,
-        "cam"       => 0,
-        "fake"      => 1,
-        "warp"      => 0,
-        "stack"     => 0,
-        "staticsky" => 0,
-        "fullforce" => 0,
-        "diff"      => 0,
-        "dist"      => 0,
-        "pub"       => 0,
-);	     
+$stages = [
+    "chip",
+    "cam",
+    "fake",
+    "warp",
+    "stack",
+    "staticsky",
+    "fullforce",
+    "diff",
+    "dist",
+    "pub",
+];
+
+$getFailures = [
+    "chip"      => "failedChipProcessedImfile.php",
+    "cam"       => "failedCamProcessedExp.php",
+    "fake"      => "failedFakeProcessedImfile.php",
+    "warp"      => "failedWarpSkyfiles.php",
+    "stack"     => "failedStackSkyfile.php",
+    "staticsky" => "failedStaticsky.php",
+    "fullforce" => "failedFullForceSkyfiles.php",
+    "diff"      => "failedDiffSkyfile.php",
+    "dist"      => "NONE",
+    "pub"       => "publishDone.php",
+];
+
+$mainTable = [
+    "chip"      => "chipRun",
+    "cam"       => "camRun",
+    "fake"      => "fakeRun",
+    "warp"      => "warpRun",
+    "stack"     => "stackRun",
+    "staticsky" => "staticskyRun",
+    "fullforce" => "fullForceRun",
+    "diff"      => "diffRun",
+    "dist"      => "distRun",
+    "pub"       => "publishRun",
+];
+
+$addSearchState = [
+    "chip"      => 0,
+    "cam"       => 0,
+    "fake"      => 1,
+    "warp"      => 0,
+    "stack"     => 0,
+    "staticsky" => 0,
+    "fullforce" => 0,
+    "diff"      => 0,
+    "dist"      => 0,
+    "pub"       => 0,
+];
 
 $pass = $ID['pass'];
@@ -159,5 +159,5 @@
 
 // The Update mode of czartool is obsolete. selectedMode is hardcoded to stdscience above.
-$isUpdate = ($selectedMode == "update");
+// $isUpdate = ($selectedMode == "update");
 $isUpdate = 0;
 
@@ -211,53 +211,76 @@
 echo $table;
 createTableTitle("Status and links", 2);
-echo "<tr><td>ippMonitor Notes </td> 
-          <td> PHP: v.$phpversion 
-  &nbsp|&nbsp <a href=\"$svnlink/browser/trunk/ippMonitor\" target=new><font color=\"blue\">SVN: v.$svnversion</font></a>
-
-  &nbsp|&nbsp <a href=\"http://ippmonitor.ipp.ifa.hawaii.edu/ippMonitor/czartool_labels.php?pass=$pass&proj=$proj\"><font color=\"blue\">online (ipp113) </font></a>,
-       &nbsp <a href=\"http://ipp117.ipp.ifa.hawaii.edu/ippMonitor/czartool_labels.php?pass=$pass&proj=$proj\"><font color=\"blue\">dev (ipp117) </font></a>
-       version </td></tr>"; 
-echo "<tr><td>Pan-STARRS Data Store</td>
-          <td><a href=\"https://svn.ifa.hawaii.edu/\" target=new><font color=\"blue\">Home</font></a>
-  &nbsp|&nbsp <a href=\"https://svn.ifa.hawaii.edu/summit/ds/gpc1/\" target=new><font color=\"blue\">GPC1</font></a>
-  &nbsp|&nbsp <a href=\"https://svn.ifa.hawaii.edu/conductorb/ds/gpc2/\" target=new><font color=\"blue\">GPC2</font></a>
-  &nbsp|&nbsp <a href=\"http://ipp.ifa.hawaii.edu/skycoverage.20201110\" target=new><font color=\"blue\">Sky Coverage</font></a></td></tr>";
-echo "<tr><td>Observations Metrics</td>
-          <td><a href=\"https://ps1wiki.ifa.hawaii.edu/data/metrics/index.html\" target=new><font color = \"blue\"> PS1</font></a>
-  &nbsp|&nbsp <a href=\"https://ps1wiki.ifa.hawaii.edu/data/ps2metrics/index.html\"  target=new><font color = \"blue\"> PS2</font></a>
-  &nbsp|&nbsp <a href=\"http://ippmonitor.ipp.ifa.hawaii.edu/ippMonitor/ippMetrics/index.html\" target=new><font color=\"blue\">IPP</font></a></td></tr>";
-echo "<tr><td>Postage Stamp Server</td>
-          <td><a href=\"http://pstamp.ipp.ifa.hawaii.edu/pshome.php\" target=new><font color=\"blue\">Home</font></a>
-  &nbsp|&nbsp <a href=\"http://pstamp.ipp.ifa.hawaii.edu/status.php\" target=new><font color=\"blue\">Status</font></a>
-  &nbsp|&nbsp <a href=\"http://pstamp.ipp.ifa.hawaii.edu/dsroot.php\" target=new><font color=\"blue\">Data Store</font></a></td></tr>";
-echo "<tr><td>IPP Cluster Ganglia Monitor</td>
-          <td><a href=\"$ganglia/?r=hour&s=descending&hc=6&c=IPP%2520Production\" target=new> <font color=\"blue\">Load_reports</font></a> 
-  &nbsp|&nbsp <a href=\"$ganglia/?m=cpu_report&r=hour&s=by%2520name&c=IPP+Production&h=&sh=1&hc=8&z=small\" target=new> <font color=\"blue\">CPU_reports</font></a> 
-  &nbsp|&nbsp <a href=\"$ganglia/?m=mem_report&r=hour&s=by%2520name&c=IPP+Production&h=&sh=1&hc=8&z=small\" target=new> <font color=\"blue\">Memory_reports</font></a>
-  </td></tr>";
-echo "<tr><td>Documentations for IPP</td>
-          <td><a href=\"$conflink/wiki/spaces/IPPCZAR/pages/679903282/Czarblog+Roll\" target=new><font color=\"blue\">Czarblog</font></a>
-  &nbsp|&nbsp <a href=\"$conflink/wiki/spaces/IPPCZAR/pages/678634250/Czar+Guidelines\" target=new><font color=\"blue\">Czar guidelines</font></a>
-  &nbsp|&nbsp <a href=\"$conflink/browse/IPP-86\" target=new><font color=\"blue\">ippMonitor Jira</font></a> 
-  &nbsp|&nbsp <a href=\"$conflink/wiki/spaces/IKB/pages/678298859/IPP+Hardware+Status\" target=new><font color=\"blue\">Hardware</font></a> 
-  &nbsp|&nbsp <a href=\"$svnlink/\" target=new><font color=\"blue\">trac</font></a>
-  </td></tr>";
-echo "<tr><td>Current nightly science status</td> 
-          <td>".getNightlyScienceStatus($czardb, $proj)."</td></tr>"; 
-echo "<tr><td>Czardb last update time</td> 
-          <td> $hsttime1 (HST), $utctime1 (UTC), $mjdday (MJD)</td></tr>";
+
+$html = <<<HTML
+<tr>
+  <td>ippMonitor Notes</td>
+  <td>
+    PHP: v.$phpversion &nbsp|&nbsp 
+    <a href="$svnlink/browser/trunk/ippMonitor"                                                     target="_blank" style="color: blue; text-decoration: none;">SVN: v.$svnversion</a> &nbsp|&nbsp 
+    <a href="./czartool_labels.php?pass=$pass&proj=$proj"                                           target="_blank" style="color: blue; text-decoration: none;">online (grens1)</a> &nbsp|&nbsp
+    <a href="http://ipp117.ipp.ifa.hawaii.edu/ippMonitor/czartool_labels.php?pass=$pass&proj=$proj" target="_blank" style="color: blue; text-decoration: none;">dev (ipp117)</a> version
+  </td>
+</tr>
+<tr>
+  <td>Pan-STARRS Data Store</td>
+  <td>
+    <a href="https://svn.ifa.hawaii.edu/"                    target="_blank" style="color: blue; text-decoration: none;">Home</a> &nbsp|&nbsp
+    <a href="https://svn.ifa.hawaii.edu/summit/ds/gpc1/"     target="_blank" style="color: blue; text-decoration: none;">GPC1</a> &nbsp|&nbsp
+    <a href="https://svn.ifa.hawaii.edu/conductorb/ds/gpc2/" target="_blank" style="color: blue; text-decoration: none;">GPC2</a> &nbsp|&nbsp
+    <a href="http://ipp.ifa.hawaii.edu/skycoverage.20201110" target="_blank" style="color: blue; text-decoration: none;">Sky Coverage</a>
+  </td>
+</tr>
+<tr>
+  <td>Observations Metrics</td>
+  <td>
+    <a href="https://ps1wiki.ifa.hawaii.edu/data/metrics/index.html"    target="_blank" style="color: blue; text-decoration: none;">PS1</a> &nbsp|&nbsp
+    <a href="https://ps1wiki.ifa.hawaii.edu/data/ps2metrics/index.html" target="_blank" style="color: blue; text-decoration: none;">PS2</a> &nbsp|&nbsp
+    <a href="./ippMetrics/index.html"                                   target="_blank" style="color: blue; text-decoration: none;">IPP</a>
+  </td>
+</tr>
+<tr>
+  <td>Postage Stamp Server</td>
+  <td>
+    <a href="http://pstamp.ipp.ifa.hawaii.edu/pshome.php" target="_blank" style="color: blue; text-decoration: none;">Home</a> &nbsp|&nbsp
+    <a href="http://pstamp.ipp.ifa.hawaii.edu/status.php" target="_blank" style="color: blue; text-decoration: none;">Status</a> &nbsp|&nbsp
+    <a href="http://pstamp.ipp.ifa.hawaii.edu/dsroot.php" target="_blank" style="color: blue; text-decoration: none;">Data Store</a>
+  </td>
+</tr>
+<tr>
+  <td>IPP Cluster Ganglia Monitor</td>
+  <td>
+    <a href="$ganglia/?r=hour&s=descending&hc=6&c=IPP%2520Production"                           target="_blank" style="color: blue; text-decoration: none;">Load_reports</a> &nbsp|&nbsp
+    <a href="$ganglia/?m=cpu_report&r=hour&s=by%2520name&c=IPP+Production&h=&sh=1&hc=8&z=small" target="_blank" style="color: blue; text-decoration: none;">CPU_reports</a> &nbsp|&nbsp
+    <a href="$ganglia/?m=mem_report&r=hour&s=by%2520name&c=IPP+Production&h=&sh=1&hc=8&z=small" target="_blank" style="color: blue; text-decoration: none;">Memory_reports</a>
+  </td>
+</tr>
+<tr>
+  <td>Documentations for IPP</td>
+  <td>
+    <a href="$conflink/wiki/spaces/IPPCZAR/pages/679903282/Czarblog+Roll"                    target="_blank" style="color: blue; text-decoration: none;">Czarblog</a> &nbsp|&nbsp
+    <a href="$conflink/wiki/spaces/IPPCZAR/pages/678634250/Czar+Guidelines"                  target="_blank" style="color: blue; text-decoration: none;">Czar guidelines</a> &nbsp|&nbsp
+    <a href="$conflink/issues/?jql=project+%3D+%22IPP%22+AND+component+%3D+%22ippMonitor%22" target="_blank" style="color: blue; text-decoration: none;">ippMonitor Jira</a> &nbsp|&nbsp
+    <a href="$conflink/wiki/spaces/IKB/pages/678298859/IPP+Hardware+Status"                  target="_blank" style="color: blue; text-decoration: none;">Hardware</a> &nbsp|&nbsp
+    <a href="$svnlink/"                                                                      target="_blank" style="color: blue; text-decoration: none;">trac</a>
+  </td>
+</tr>
+
+HTML;
+echo $html;
+echo "<tr><td>Current nightly science status</td> <td>".getNightlyScienceStatus($czardb, $proj)."</td></tr>"; 
+echo "<tr><td>Czardb last update time</td> <td>$hsttime1 (HST), $utctime1 (UTC), $mjdday (MJD)</td></tr>";
 echo "</table><br>";
 
-
 createLabelsTable($pass, $proj, $czardb, $selectedMode, $labels, $distLabelsExist, $pubLabelsExist, $stages, "new", $selectedLabel, $selectedStage, $plotType);
-if ($selectedMode == "update") {echo "<br>"; createPStampDataTable();} 
-
-# XXX EAM : 2017.09.17 : debugging slowness
-# CCL : 2019.11.09 : debugging slowness
-createSummitDataTable2($projectdb, $pass, $proj, $debug); // this is slow because of the join between summitExp and rawExp, but not the major one
+
+createSummitDataTable($projectdb, $pass, $proj, $debug); 
 
 createChunkDataTable($projectdb, $debug);  // this is another part for slowness during nightly processing
 
-// createDatesTable($czardb, $proj); what is this for?
+// Postage stamp requests list 
+// createPStampDataTable();
+
+// Current dates assigned for selected pantasks servers
+// createDatesTable($czardb, $proj);
 
 // Database status
@@ -266,5 +289,5 @@
     $start = microtime(true);
     echo "<tr>";
-    createTableTitle("Database status (update fix log <a href=\"http://svn.pan-starrs.ifa.hawaii.edu/trac/ipp/wiki/Replication_Issues\"><font color=\"blue\">here</a>)", 3);
+    createTableTitle("Database status (update fix log <a href=\"http://svn.pan-starrs.ifa.hawaii.edu/trac/ipp/wiki/Replication_Issues\" target=_blank><font color=\"blue\">here</a>)", 3);
       
     echo "<tr>";
@@ -324,5 +347,5 @@
     showPingStatus($HOST_NAGIOS, $LABEL_NAGIOS);
     showPingStatus($HOST_SVN, $LABEL_SVN);
-    showPingStatus($HOST_CONFLUENCE, $LABEL_CONFLUENCE);
+    //showPingStatus($HOST_CONFLUENCE, $LABEL_CONFLUENCE);
     showPingStatus($HOST_MAILMAN, $LABEL_MAILMAN);
 
@@ -339,9 +362,10 @@
 
 #2nd table column empty contnets
-echo "<td style=height:200px;width: 10px;text-align:top;\"><br>";
+//echo "<td style=height:200px;width: 10px;text-align:top;\"><br>";
 
 #3rd table column for plots
 echo "<td style=height:200px;width:600px;text-align:top;\"><br>";
-# plots
+
+// processing times series plots
 if ($selectedStage == "all_stages" && $selectedLabel == "all") {
     // Use old method with gnuplots as the rate query is slow with all stages and labels
@@ -351,6 +375,6 @@
 
     // Uncomment for dev version with Google Charts
-    // echo Gettimeseries($czardb, $selectedMode, $selectedLabel, $proj, $selectedStage);
-    //echo Getrate($czardb, $selectedMode, $selectedLabel, $proj, $selectedStage);
+    # echo Gettimeseries($czardb, $selectedMode, $selectedLabel, $proj, $selectedStage);
+    # echo Getrate($czardb, $selectedMode, $selectedLabel, $proj, $selectedStage);
 } else {
     // Use Google Charts for specific stage and label
@@ -359,5 +383,36 @@
 }
 
-// Helper function to generate image tags for plots
+require_once 'functions.php';  
+// data quality for current day
+echo getDataQuality($projectdb, 'today', $proj); 
+
+// storage timeseries
+echo getSpaces($czardb);
+
+// storage plot
+echo getHosts($czardb);
+
+
+# timer for main page end
+echo "<tr><th colspan=3>";
+echo "overall loading in " . round((microtime(true) - $mainstart), 3) . " seconds.";
+echo "</th></tr>";
+echo "</table>";
+
+menu_end();
+
+
+/////////////////////////////////////////////////////////////////////////////
+//                                                                         //
+//                               Functions                                 //
+//                                                                         //
+/////////////////////////////////////////////////////////////////////////////
+
+###########################################################################
+#
+# Display images
+# Helper function to generate image tags for plots
+#
+###########################################################################
 function displayImage($type, $mode, $label, $proj, $stage, $plottype) {
     $src = "czartool_getplot.php?mode=" . urlencode($mode) .
@@ -370,26 +425,4 @@
 }
 
-# data quality timeseries
-echo getDataQuality2($projectdb); // new version of data quality plots
-
-# storage timeseries
-echo getSpaces($czardb);
-# storage plot
-echo getHosts($czardb);
-
-# timer for main page end
-echo "<tr><th colspan=3>";
-echo "overall loading in " . round((microtime(true) - $mainstart), 3) . " seconds.";
-echo "</th></tr>";
-echo "</table>";
-
-menu_end();
-
-
-/////////////////////////////////////////////////////////////////////////////
-//                                                                         //
-//                               Functions                                 //
-//                                                                         //
-/////////////////////////////////////////////////////////////////////////////
 
 ###########################################################################
@@ -399,13 +432,35 @@
 ###########################################################################
 function createPStampDataTable() {
-
-    $sql = "SELECT label, COUNT(job_id) AS 'Unfinished Jobs', IFNULL(priority,10000) AS priority FROM pstampRequest JOIN pstampJob USING(req_id) LEFT JOIN Label USING(label) WHERE pstampJob.state ='run' and pstampRequest.state ='run' GROUP by label";
-
-    $pstampdb = DB::connect("mysql://ippuser:ippuser@ipp113/ippRequestServer");
-    $qry = $pstampdb->query($sql);
-    if (dberror($qry)) {
+    // timer start
+    $start = microtime(true);
+
+    $dsn = "mysql:host=ipp113;dbname=ippRequestServer;charset=utf8mb4";
+    $pdo = new PDO($dsn, 'ippuser', 'ippuser', [
+        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+    ]);
+
+    // SQL statement
+    $sql = "
+        SELECT label,
+               COUNT(job_id) AS `Unfinished Jobs`,
+               IFNULL(priority,10000) AS priority
+        FROM pstampRequest
+        JOIN pstampJob USING(req_id)
+        LEFT JOIN Label USING(label)
+        WHERE pstampJob.state ='run'
+          AND pstampRequest.state ='run'
+        GROUP BY label
+    ";
+   
+    try {
+        // Execute the query
+        $stmt = $pdo->query($sql);
+        $rows = $stmt->fetchAll(PDO::FETCH_NUM);
+
+    } catch (PDOException $e) {
         echo "<b>error reading pstamp table</b><br>\n";
-        echo "<br><small><b> table query : $sql </b></small><br>\n";
+        echo "<br><small><b> table query : " . htmlspecialchars($sql) . " </b></small><br>\n";
         menu_end();
+        return; // stop executing the rest of the function
     }
 
@@ -414,5 +469,4 @@
     echo $table;
 
-    #echo "<table width=\"$width\" align=\"middle\" border=\"1\" cellspacing=\"0\" cellpadding=\"4\" style=\"font-size:80%\">\n";
     echo "<tr>";
     createTableTitle("Postage stamp requests", 3);
@@ -423,14 +477,18 @@
 
     // list the results
-    while ($qry->fetchInto($row)) {
+    foreach ($rows as $row) {
         echo "<tr>";
-        echo "<td>$row[0]</td>";
-        echo "<td>$row[1]</td>";
-        echo "<td>$row[2]</td>";
+        echo "<td>" . htmlspecialchars($row[0]) . "</td>";
+        echo "<td>" . htmlspecialchars($row[1]) . "</td>";
+        echo "<td>" . htmlspecialchars($row[2]) . "</td>";
         echo "</tr>";
     }
-    echo "</table>\n";
-}
-
+    
+    // End timer
+    echo "<tr><th colspan=10>";
+    $total_time = round(microtime(true) - $start, 3);
+    echo "<center>loading in $total_time seconds<br></center>";
+    echo "</table><br>";
+}
 
 ###########################################################################
@@ -448,51 +506,109 @@
     $mjdDay  = getMJD();
     $expname = "o{$mjdDay}%";
-
     $date = gmdate("Y-m-d");
-    $sql = "SELECT MIN(exp_id) FROM rawExp WHERE exp_name like '$expname' ";
-    if ($debug) {echo "$sql<br>";}
-    $qry = $projectdb->query($sql);
-
-    if (dberror($qry)) {
+
+    $sql = "SELECT MIN(exp_id) FROM rawExp WHERE exp_name LIKE :expname";
+    if ($debug) {
+        echo "$sql<br>";
+    }
+    
+    try {
+        // Prepare and execute
+        $stmt = $projectdb->prepare($sql);
+        $stmt->execute([':expname' => $expname]);
+    
+        // Fetch the single row
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+    
+        if ($row === false || $row[0] === null) {
+            echo "*** WARNING: No data for tonight **\n";
+            $refExpID = 0;
+        } else {
+            $refExpID = $row[0];
+        }
+    } catch (PDOException $e) {
         echo "*** WARNING: No data for tonight **\n";
         $refExpID = 0;
-    } else {
-        $qry->fetchInto($refExpID);
-        $refExpID = isset($refExpID[0]) ? $refExpID[0] : 0;
-    }
+    
+        // (optional) debug
+        if ($debug) {
+            echo "DB error: " . $e->getMessage() . "<br>";
+        }
+    }
+
     // Print the result of the query
-    if ($debug) {echo "The first exp_id of tonight is: $refExpID<br>";}
-
-    #do a query of raw/chip/cam/warp counts for this night, split by chunk. 
-    #we need to do a subquery to find the total number of skycells in warpSkyfile, as well as the number of those with a bad quality
-    $query = "SELECT suba.chunk,suba.Nvis1,suba.Nvis2,suba.Nvis3,suba.Nvis4,suba.Ncamgood,suba.Ncambad,subb.Nwarpgood,subb.Nwarpbad FROM ";
-    $query .= "    (SELECT substr(comment, 1, position(' ' in comment)) AS chunk,count(if(rawExp.comment LIKE '%visit 1%',1,NULL)) AS Nvis1,";
-    $query .= "    count(if(rawExp.comment LIKE '%visit 2%',1,NULL)) AS Nvis2,count(if(rawExp.comment LIKE '%visit 3%',1,NULL)) AS Nvis3,";
-    $query .= "    count(if(rawExp.comment LIKE '%visit 4%',1,NULL)) AS Nvis4, count(if(camProcessedExp.sigma_ra <= 5 AND camProcessedExp.sigma_dec <= 5 AND camProcessedExp.quality = 0,1,NULL)) AS Ncamgood,";
-    $query .= "    count(if(camProcessedExp.fwhm_major > 12 OR camProcessedExp.sigma_ra > 5 OR camProcessedExp.sigma_dec > 5 OR camProcessedExp.quality > 0,1,NULL)) AS Ncambad";
-    $query .= "    FROM rawExp LEFT JOIN chipRun USING (exp_id) LEFT JOIN camRun USING (chip_id) JOIN camProcessedExp USING (cam_id) LEFT JOIN fakeRun USING (cam_id) LEFT JOIN warpRun USING (fake_id)";
-    $query .= "    WHERE exp_name like '$expname' AND exp_type = 'OBJECT' AND rawExp.comment like '%visit%'";
-    $query .= "    AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-    $query .= "    GROUP BY chunk ORDER BY dateobs) as suba ";
-    $query .= "LEFT JOIN";
-    $query .= "    (SELECT subc.chunk, count(if(subc.Nwarpskycellbad < subc.Nwarpskycell,1,NULL)) AS Nwarpgood, count(if(subc.Nwarpskycellbad = subc.Nwarpskycell AND subc.Nwarpskycell > 0,1,NULL)) AS Nwarpbad";
-    $query .= "    FROM ";
-    $query .= "      (SELECT substr(comment, 1, position(' ' in comment)) AS chunk,count(warpSkyfile.quality) AS Nwarpskycell,count(if(warpSkyfile.quality > 0,1,NULL)) AS Nwarpskycellbad";
-    $query .= "      FROM rawExp LEFT JOIN chipRun USING (exp_id) LEFT JOIN camRun USING (chip_id) LEFT JOIN fakeRun USING (cam_id) LEFT JOIN warpRun USING (fake_id) JOIN warpSkyfile USING (warp_id)";
-    $query .= "      WHERE exp_name like '$expname' AND exp_type = 'OBJECT' AND rawExp.comment like '%visit%'";
-    $query .= "      AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-    $query .= "      GROUP BY exp_name) as subc";
-    $query .= "    GROUP BY chunk) AS subb ";
-    $query .= "ON suba.chunk=subb.chunk";
- 
-    if ($debug) {echo "$query<br>";}
-    $qry = $projectdb->query($query);
-
-    if (dberror($qry)) {
+    if ($debug) {
+        echo "The first exp_id of tonight is: $refExpID<br>";
+    }
+
+    // Build the query (same SQL, but parameterized for $expname)
+    $query = "SELECT suba.chunk,suba.Nvis1,suba.Nvis2,suba.Nvis3,suba.Nvis4,
+                     suba.Ncamgood,suba.Ncambad,subb.Nwarpgood,subb.Nwarpbad
+              FROM (
+                  SELECT substr(comment, 1, position(' ' in comment)) AS chunk,
+                         count(if(rawExp.comment LIKE '%visit 1%',1,NULL)) AS Nvis1,
+                         count(if(rawExp.comment LIKE '%visit 2%',1,NULL)) AS Nvis2,
+                         count(if(rawExp.comment LIKE '%visit 3%',1,NULL)) AS Nvis3,
+                         count(if(rawExp.comment LIKE '%visit 4%',1,NULL)) AS Nvis4,
+                         count(if(camProcessedExp.sigma_ra <= 5
+                                  AND camProcessedExp.sigma_dec <= 5
+                                  AND camProcessedExp.quality = 0,1,NULL)) AS Ncamgood,
+                         count(if(camProcessedExp.fwhm_major > 12
+                                  OR camProcessedExp.sigma_ra > 5
+                                  OR camProcessedExp.sigma_dec > 5
+                                  OR camProcessedExp.quality > 0,1,NULL)) AS Ncambad
+                  FROM rawExp
+                       LEFT JOIN chipRun USING (exp_id)
+                       LEFT JOIN camRun USING (chip_id)
+                       JOIN camProcessedExp USING (cam_id)
+                       LEFT JOIN fakeRun USING (cam_id)
+                       LEFT JOIN warpRun USING (fake_id)
+                  WHERE exp_name LIKE :expname
+                    AND exp_type = 'OBJECT'
+                    AND rawExp.comment LIKE '%visit%'
+                    AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+                    AND obs_mode NOT LIKE 'ENGINEERING'
+                    AND obs_mode NOT LIKE 'MANUAL'
+                  GROUP BY chunk ORDER BY dateobs
+              ) as suba
+              LEFT JOIN (
+                  SELECT subc.chunk,
+                         count(if(subc.Nwarpskycellbad < subc.Nwarpskycell,1,NULL)) AS Nwarpgood,
+                         count(if(subc.Nwarpskycellbad = subc.Nwarpskycell
+                                  AND subc.Nwarpskycell > 0,1,NULL)) AS Nwarpbad
+                  FROM (
+                      SELECT substr(comment, 1, position(' ' in comment)) AS chunk,
+                             count(warpSkyfile.quality) AS Nwarpskycell,
+                             count(if(warpSkyfile.quality > 0,1,NULL)) AS Nwarpskycellbad
+                      FROM rawExp
+                           LEFT JOIN chipRun USING (exp_id)
+                           LEFT JOIN camRun USING (chip_id)
+                           LEFT JOIN fakeRun USING (cam_id)
+                           LEFT JOIN warpRun USING (fake_id)
+                           JOIN warpSkyfile USING (warp_id)
+                      WHERE exp_name LIKE :expname
+                        AND exp_type = 'OBJECT'
+                        AND rawExp.comment LIKE '%visit%'
+                        AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+                        AND obs_mode NOT LIKE 'ENGINEERING'
+                        AND obs_mode NOT LIKE 'MANUAL'
+                      GROUP BY exp_name
+                  ) as subc
+                  GROUP BY chunk
+              ) AS subb
+              ON suba.chunk=subb.chunk";
+    
+    if ($debug) { echo "$query<br>"; }
+    
+    try {
+        $stmt = $projectdb->prepare($query);
+        $stmt->execute([':expname' => $expname]);
+    } catch (PDOException $e) {
         echo "<b>error querying SQL table</b><br>\n";
         echo "<br><small><b> table query : $query </b></small><br>\n";
         menu_end();
-    }
-
+        exit;
+    }
+    
     // set up the table
     global $table;
@@ -512,116 +628,139 @@
     createTableColumnHeader("wsd_pub");
     createTableColumnHeader("Completion");
-
+    
     $msg = "No science images taken since $date";
-
+    
+    $badcamflag  = 0;
+    $badwarpflag = 0;
+    $baddiffflag = 0;
     // list the results
-    while ($qry->fetchInto($row)) {
-        $Nvis1 = 0;
-        $Nvis2 = 0;
-        $Nvis3 = 0;
-        $Nvis4 = 0;
-        $Ncamgood = 0;
-        $Ncambad = 0;
-        $Nwarpgood = 0;
-        $Nwarpbad = 0;
+    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+	$chunkname = $row['chunk']    ?? '';
+        $Nvis1     = $row['Nvis1']     ?? 0;
+        $Nvis2     = $row['Nvis2']     ?? 0;
+        $Nvis3     = $row['Nvis3']     ?? 0;
+        $Nvis4     = $row['Nvis4']     ?? 0;
+        $Ncamgood  = $row['Ncamgood']  ?? 0;
+        $Ncambad   = $row['Ncambad']   ?? 0;
+        $Nwarpgood = $row['Nwarpgood'] ?? 0;
+        $Nwarpbad  = $row['Nwarpbad']  ?? 0;
+
+        if($Ncambad  >= 1) $badcamflag  = 1;
+        if($Nwarpbad >= 1) $badwarpflag = 1;
+
+        $query1 = "SELECT suba.chunk,
+                          SUM(IF(suba.Nvisits = 2,1,NULL)) as totvis2,
+                          SUM(IF(suba.Nvisits = 3 OR suba.Nvisits = 4,2,NULL)) as totvis34
+                   FROM (
+                       SELECT substr(comment, 1, position(' ' in comment)) AS chunk,
+                              count(distinct(comment)) AS Nvisits
+                       FROM rawExp 
+                            LEFT JOIN chipRun USING (exp_id)
+                            LEFT JOIN camRun USING (chip_id)
+                       WHERE exp_name LIKE :expname
+                         AND exp_type = 'OBJECT'
+                         AND rawExp.comment LIKE '%visit%'
+                         AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+                         AND obs_mode NOT LIKE 'ENGINEERING'
+                         AND obs_mode NOT LIKE 'MANUAL'
+                         AND substr(comment,1,position(' ' in comment)) LIKE :chunkname
+                       GROUP BY object
+                   ) as suba
+                   GROUP BY chunk";
+        
+        $stmt1 = $projectdb->prepare($query1);
+        $stmt1->execute([':expname'=>$expname, ':chunkname'=>"$chunkname%"]);
+        $row1 = $stmt1->fetch(PDO::FETCH_ASSOC);
         $NexpWWdiff = 0;
-        $Npubwarps = 0;
-
-        $chunkname = $row[0];
-        if($row[1] > 0) $Nvis1 = $row[1];
-        if($row[2] > 0) $Nvis2 = $row[2];
-        if($row[3] > 0) $Nvis3 = $row[3];
-        if($row[4] > 0) $Nvis4 = $row[4];
-        if($row[5] > 0) $Ncamgood = $row[5];
-        if($row[6] > 0) $Ncambad = $row[6];
-        if($row[7] > 0) $Nwarpgood = $row[7];
-        if($row[8] > 0) $Nwarpbad = $row[8];
-
-        #set flags if present
-        if($Ncambad >= 1) $badcamflag = 1;
-        if($Nwarpbad >= 1) $badwarpflag = 1;
-
-        $query1 = "SELECT suba.chunk, SUM(IF(suba.Nvisits = 2,1,NULL)) as totvis2, SUM(IF(suba.Nvisits = 3 OR suba.Nvisits = 4,2,NULL)) as totvis34 FROM";
-        $query1 .= "    (SELECT substr(comment, 1, position(' ' in comment)) AS chunk,count(distinct(comment)) AS Nvisits FROM";
-        $query1 .= "    rawExp LEFT JOIN chipRun USING (exp_id) LEFT JOIN camRun USING (chip_id) ";
-        $query1 .= "    WHERE exp_name like '$expname'";
-        $query1 .= "    AND exp_type = 'OBJECT' AND rawExp.comment like '%visit%' AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-        $query1 .= "    AND substr(comment, 1, position(' ' in comment)) LIKE '$chunkname%' group by object) as suba group by chunk;";
-
-        $qry1 = $projectdb->query($query1);
-        while ($qry1->fetchInto($row)) {$NexpWWdiff = $row[1] + $row[2];}
-
-        #find the unique diff_ids for each chunk, to be matched to the diffRun catalog
-        $query2 = "     SELECT diff_id,warp1,stack1,warp2,stack2 FROM ";
-        $query2 .= "           diffInputSkyfile JOIN warpRun ON (warp1=warp_id OR warp2=warp_id) JOIN fakeRun USING (fake_id) JOIN camRun USING (cam_id) JOIN camProcessedExp USING (cam_id) JOIN chipRun USING (chip_id) JOIN rawExp USING (exp_id)";
-        $query2 .= "           WHERE exp_name like '$expname'";
-        $query2 .= "           AND exp_type = 'OBJECT' AND rawExp.comment like '%visit%' AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')";
-        $query2 .= "            AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL' AND substr(comment, 1, position(' ' in comment)) LIKE '$chunkname%' GROUP By diff_id";
-        $qry2 = $projectdb->query($query2);
-
-        $NWWdiffgood = 0;
-        $NWWdiffbad = 0;
-        $NWWdiffpub = 0;
-
-        $NWSdiffgood = 0;
-        $NWSdiffbad = 0;
-        $NWSdiffpub = 0;
-        $wrpcnt = array();
-
-        while ($qry2->fetchInto($row2)) {
-              #distinguish between WW and WS diffs based on stack2
-              if ($row2[4] == 0) {
-                  #find the total number of skycell and the number of skycell with a bad quality flag as well as the full publish state 
-                  $query3 = "     SELECT count(diffSkyfile.quality) AS Ndiff,count(if(diffSkyfile.quality > 0,1,NULL)) AS Ndiffbad,count(if(publishRun.state LIKE 'full',1,NULL)) AS Npub FROM ";
-                  $query3 .= "           diffRun JOIN diffSkyfile USING (diff_id) LEFT JOIN publishRun ON (diffRun.diff_id = stage_id)";
-                  $query3 .= "           WHERE diff_id='$row2[0]' GROUP BY diffRun.diff_id";
-                  $qry3 = $projectdb->query($query3);
-
-                  while ($qry3->fetchInto($row3)) {
-                      #if all skycells have bad quality, this is a bad diff. If all the skycells are in a full state in publishRun, it is fully published 
-                      if ($row3[1]<$row3[0]) $NWWdiffgood ++;
-                      if ($row3[1]==$row3[0]) $NWWdiffbad ++;
-                      if ($row3[2]==$row3[0]) $NWWdiffpub ++;
-
-                      #if this wwdiff is published, record its corresponding warp_ids to see how many unique ones there are
-                      if ($row3[2]==$row3[0]) $wrpcnt{$row2[1]}++;  # record the presence of warp1
-                      if ($row3[2]==$row3[0]) $wrpcnt{$row2[3]}++;  # record the presence of warp2
-                  }
-              }
-
-              if ($row2[4] > 0) {
-                  #find the total number of skycell and the number of skycell with a bad quality flag as well as the full publish state 
-                  $query3 = "     SELECT count(diffSkyfile.quality) AS Ndiff,count(if(diffSkyfile.quality > 0,1,NULL)) AS Ndiffbad,count(if(publishRun.state LIKE 'full',1,NULL)) AS Npub FROM ";
-                  $query3 .= "           diffRun JOIN diffSkyfile USING (diff_id) LEFT JOIN publishRun ON (diffRun.diff_id = stage_id)";
-                  $query3 .= "           WHERE diff_id='$row2[0]' GROUP BY diffRun.diff_id";
-                  $qry3 = $projectdb->query($query3);
-
-                  while ($qry3->fetchInto($row3)) {
-                      #if all skycells have bad quality, this is a bad diff. If all the skycells are in a full state in publishRun, it is fully published 
-                      if ($row3[1]<$row3[0]) $NWSdiffgood ++;
-                      if ($row3[1]==$row3[0]) $NWSdiffbad ++;
-                      if ($row3[2]==$row3[0]) $NWSdiffpub ++;
-                  }
-              }
+        if ($row1) {
+            $NexpWWdiff = ($row1['totvis2'] ?? 0) + ($row1['totvis34'] ?? 0);
         }
-        #pull out the unique warp_ids
+
+	$query2 = "SELECT diff_id,warp1,stack1,warp2,stack2
+           FROM diffInputSkyfile
+                JOIN warpRun ON (warp1=warp_id OR warp2=warp_id)
+                JOIN fakeRun USING (fake_id)
+                JOIN camRun USING (cam_id)
+                JOIN camProcessedExp USING (cam_id)
+                JOIN chipRun USING (chip_id)
+                JOIN rawExp USING (exp_id)
+           WHERE exp_name LIKE :expname
+             AND exp_type = 'OBJECT'
+             AND rawExp.comment LIKE '%visit%'
+             AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+             AND obs_mode NOT LIKE 'ENGINEERING'
+             AND obs_mode NOT LIKE 'MANUAL'
+             AND substr(comment,1,position(' ' in comment)) LIKE :chunkname
+           GROUP BY diff_id";
+
+        $stmt2 = $projectdb->prepare($query2);
+        $stmt2->execute([':expname'=>$expname, ':chunkname'=>"$chunkname%"]);
+        
+        $NWWdiffgood = $NWWdiffbad = $NWWdiffpub = 0;
+        $NWSdiffgood = $NWSdiffbad = $NWSdiffpub = 0;
+        $wrpcnt = [];
+        
+        while ($row2 = $stmt2->fetch(PDO::FETCH_ASSOC)) {
+            $diff_id = $row2['diff_id'];
+            $warp1   = $row2['warp1'];
+            $warp2   = $row2['warp2'];
+            $stack2  = $row2['stack2'];
+        
+            // Build $query3 (same for WW and WS)
+            $query3 = "SELECT count(diffSkyfile.quality) AS Ndiff,
+                              count(if(diffSkyfile.quality > 0,1,NULL)) AS Ndiffbad,
+                              count(if(publishRun.state LIKE 'full',1,NULL)) AS Npub
+                       FROM diffRun
+                            JOIN diffSkyfile USING (diff_id)
+                            LEFT JOIN publishRun ON (diffRun.diff_id = stage_id)
+                       WHERE diff_id=:diffid
+                       GROUP BY diffRun.diff_id";
+        
+            $stmt3 = $projectdb->prepare($query3);
+            $stmt3->execute([':diffid'=>$diff_id]);
+            $row3 = $stmt3->fetch(PDO::FETCH_ASSOC);
+            if (!$row3) continue;
+        
+            $Ndiff    = $row3['Ndiff']    ?? 0;
+            $Ndiffbad = $row3['Ndiffbad'] ?? 0;
+            $Npub     = $row3['Npub']     ?? 0;
+        
+            if ($stack2 == 0) {
+                if ($Ndiffbad < $Ndiff) $NWWdiffgood++;
+                if ($Ndiffbad == $Ndiff) $NWWdiffbad++;
+                if ($Npub == $Ndiff) $NWWdiffpub++;
+                if ($Npub == $Ndiff) {
+                    $wrpcnt[$warp1] = ($wrpcnt[$warp1] ?? 0) + 1;
+                    $wrpcnt[$warp2] = ($wrpcnt[$warp2] ?? 0) + 1;
+                }
+            } else {
+                if ($Ndiffbad < $Ndiff) $NWSdiffgood++;
+                if ($Ndiffbad == $Ndiff) $NWSdiffbad++;
+                if ($Npub == $Ndiff) $NWSdiffpub++;
+            }
+        }
+
+	// Pull out the unique warp_ids
         $Npubwarps = count($wrpcnt);
-
-        #check for bad diffs 
-        if(($NWWdiffbad >= 1) or ($NWSdiffbad >= 1)) $baddiffflag = 1;
-
+        
+        // Check for bad diffs
+        if (($NWWdiffbad >= 1) || ($NWSdiffbad >= 1)) {
+            $baddiffflag = 1;
+        }
+        
+        // Decide on completion text
         if ($Nvis1 != $Nvis4) {
-          if ($NWWdiffpub != $NexpWWdiff) $complete = "chunk incomplete, not done";  
-          if ($NWWdiffpub == $NexpWWdiff) $complete = "chunk incomplete, done";  
+            $complete = ($NWWdiffpub == $NexpWWdiff)
+                ? "chunk incomplete, done"
+                : "chunk incomplete, not done";
+        } else {
+            $complete = ($NWWdiffpub == $NexpWWdiff)
+                ? "chunk complete, done"
+                : "chunk complete, not done";
         }
-        if ($Nvis1 == $Nvis4) {
-          if ($NWWdiffpub != $NexpWWdiff) $complete = "chunk complete, not done";  
-          if ($NWWdiffpub == $NexpWWdiff) $complete = "chunk complete, done";  
-
-        }
-
+        
+        // Now output the row
         echo "<tr>";
-
-        echo "<td>$chunkname</td>";
+        echo "<td>" . htmlspecialchars($chunkname) . "</td>";
         echo "<td>$Nvis1/$Nvis2/$Nvis3/$Nvis4</td>";
         echo "<td>$Ncamgood/$Ncambad</td>";
@@ -633,13 +772,11 @@
         echo "<td>$NWSdiffpub</td>";
         echo "<td>$complete</td>";
-
         echo "</tr>";
     }
-
-
+    
     #-----------------------------------------
-    #list bad quality cam exposures
+    # list bad quality cam exposures
     if ($badcamflag != 0) {
-
+    
         // set up another table
         global $table;
@@ -654,30 +791,44 @@
         createTableColumnHeader("sigma_dec");
         createTableColumnHeader("comment");
-
-         #query the bad cam exposures
-        $query = "SELECT exp_name,cam_id, camProcessedExp.quality, camProcessedExp.sigma_ra, camProcessedExp.sigma_dec, comment";
-        $query .= " FROM rawExp";
-        $query .= " JOIN chipRun USING (exp_id)";
-        $query .= " JOIN camRun USING (chip_id)";
-        $query .= " JOIN camProcessedExp USING (cam_id)";
-        //$query .= " WHERE dateobs LIKE '$date%' AND exp_id > $refExpID"; 
-        $query .= " WHERE exp_name like '$expname'";
-        $query .= " AND exp_type = 'OBJECT'";
-        $query .= " AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-        $query .= " AND (camProcessedExp.fwhm_major > 12 OR camProcessedExp.quality > 0 OR camProcessedExp.sigma_ra > 5 OR camProcessedExp.sigma_dec > 5)";
-        $query .= " ORDER BY comment";
-
-        $qry1 = $projectdb->query($query);
-        while ($qry1->fetchInto($row)) {
-
+    
+        // query the bad cam exposures
+        $query = "
+            SELECT exp_name,
+                   cam_id,
+                   camProcessedExp.quality,
+                   camProcessedExp.sigma_ra,
+                   camProcessedExp.sigma_dec,
+                   comment
+            FROM rawExp
+            JOIN chipRun USING (exp_id)
+            JOIN camRun USING (chip_id)
+            JOIN camProcessedExp USING (cam_id)
+            WHERE exp_name LIKE :expname
+              AND exp_type = 'OBJECT'
+              AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+              AND obs_mode NOT LIKE 'ENGINEERING'
+              AND obs_mode NOT LIKE 'MANUAL'
+              AND (
+                camProcessedExp.fwhm_major > 12 OR
+                camProcessedExp.quality > 0 OR
+                camProcessedExp.sigma_ra > 5 OR
+                camProcessedExp.sigma_dec > 5
+              )
+            ORDER BY comment";
+    
+        if ($debug) { echo "$query<br>"; }
+        // prepare & execute
+        $stmt = $projectdb->prepare($query);
+        $stmt->execute([':expname' => $expname]);
+    
+        // output rows
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
             echo "<tr>";
-
-            echo "<td>$row[0]</td>";
-            echo "<td>$row[1]</td>";
-            echo "<td>$row[2]</td>";
-            echo "<td>$row[3]</td>";
-            echo "<td>$row[4]</td>";
-            echo "<td>$row[5]</td>";
-
+            echo "<td>" . htmlspecialchars($row[0]) . "</td>"; // exp_name
+            echo "<td>" . htmlspecialchars($row[1]) . "</td>"; // cam_id
+            echo "<td>" . htmlspecialchars($row[2]) . "</td>"; // quality
+            echo "<td>" . htmlspecialchars($row[3]) . "</td>"; // sigma_ra
+            echo "<td>" . htmlspecialchars($row[4]) . "</td>"; // sigma_dec
+            echo "<td>" . htmlspecialchars($row[5]) . "</td>"; // comment
             echo "</tr>";
         }
@@ -685,7 +836,7 @@
 
     #-----------------------------------------
-    #list bad quality warp exposures
+    # list bad quality warp exposures
     if ($badwarpflag != 0) {
-
+    
         // set up another table
         global $table;
@@ -697,27 +848,39 @@
         createTableColumnHeader("warp_id");
         createTableColumnHeader("comment");
-
-        #query the bad warps
-        $query = "SELECT exp_name,warp_id,count(warpSkyfile.quality) AS Nwarpskycell,count(if(warpSkyfile.quality > 0,1,NULL)) AS Nwarpskycellbad, comment";
-        $query .= "      FROM rawExp LEFT JOIN chipRun USING (exp_id) LEFT JOIN camRun USING (chip_id) LEFT JOIN fakeRun USING (cam_id) LEFT JOIN warpRun USING (fake_id) JOIN warpSkyfile USING (warp_id)";
-        $query .= "      WHERE exp_name like '$expname'";
-        $query .= "      AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-        $query .= "      GROUP BY exp_name ORDER BY comment";
-
-        $qry1 = $projectdb->query($query);
-        while ($qry1->fetchInto($row)) {
-
+    
+        // query the bad warps
+        $query = "
+            SELECT exp_name,
+                   warp_id,
+                   COUNT(warpSkyfile.quality) AS Nwarpskycell,
+                   COUNT(IF(warpSkyfile.quality > 0,1,NULL)) AS Nwarpskycellbad,
+                   comment
+            FROM rawExp
+            LEFT JOIN chipRun USING (exp_id)
+            LEFT JOIN camRun USING (chip_id)
+            LEFT JOIN fakeRun USING (cam_id)
+            LEFT JOIN warpRun USING (fake_id)
+            JOIN warpSkyfile USING (warp_id)
+            WHERE exp_name LIKE :expname
+              AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+              AND obs_mode NOT LIKE 'ENGINEERING'
+              AND obs_mode NOT LIKE 'MANUAL'
+            GROUP BY exp_name
+            ORDER BY comment";
+    
+        // prepare and execute
+        $stmt = $projectdb->prepare($query);
+        $stmt->execute([':expname' => $expname]);
+    
+        // loop through rows
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
             echo "<tr>";
-
-            echo "<td>$row[0]</td>";
-            echo "<td>$row[1]</td>";
-            echo "<td>$row[2]</td>";
-
+            echo "<td>" . htmlspecialchars($row[0]) . "</td>";  // exp_name
+            echo "<td>" . htmlspecialchars($row[1]) . "</td>";  // warp_id
+            echo "<td>" . htmlspecialchars($row[4]) . "</td>";  // comment
             echo "</tr>";
         }
     }
 
-    #-----------------------------------------
-    #list bad quality diff exposures
     if ($baddiffflag != 0) {
         // set up another table
@@ -734,64 +897,64 @@
         createTableColumnHeader("stack2");
         createTableColumnHeader("comment");
-
-        #query the bad diffs
-        $query = "     SELECT diff_id,warp1,stack1,warp2,stack2,exp_name,comment FROM ";
-        $query .= "           diffInputSkyfile JOIN warpRun ON (warp1=warp_id OR warp2=warp_id) JOIN fakeRun USING (fake_id) JOIN camRun USING (cam_id) JOIN camProcessedExp USING (cam_id) JOIN chipRun USING (chip_id) JOIN rawExp USING (exp_id)";
-        $query .= "           WHERE exp_name like '$expname'";
-        $query .= "           AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-        $query .= "           GROUP By diff_id";
-        if ($debug) {echo "$query<br>";}
-
-        $qry1 = $projectdb->query($query);
-        while ($qry1->fetchInto($row)) {
-            #distinguish between WW and WS diffs based on stack2
-            if ($row[4] == 0) {
-                  #find the total number of skycell and the number of skycell with a bad quality flag as well as the full publish state 
-                  $query3 = "     SELECT count(diffSkyfile.quality) AS Ndiff,count(if(diffSkyfile.quality > 0,1,NULL)) AS Ndiffbad,count(if(publishRun.state LIKE 'full',1,NULL)) AS Npub FROM ";
-                  $query3 .= "           diffRun JOIN diffSkyfile USING (diff_id) LEFT JOIN publishRun ON (diffRun.diff_id = stage_id)";
-                  $query3 .= "           WHERE diff_id='$row[0]' GROUP BY diffRun.diff_id";
-                  $qry3 = $projectdb->query($query3);
-
-                  while ($qry3->fetchInto($row3)) {
-                      #if all skycells have bad quality, this is a bad diff. If all the skycells are in a full state in publishRun, it is fully published 
-                      if ($row3[1]==$row3[0]) {
-                          echo "<tr>";
-
-                          echo "<td>$row[5]</td>";
-                          echo "<td>$row[0]</td>";
-                          echo "<td>$row[1]</td>";
-                          echo "<td>$row[2]</td>";
-                          echo "<td>$row[3]</td>";
-                          echo "<td>$row[4]</td>";
-                          echo "<td>$row[6]</td>";
-
-                          echo "</tr>";
-                      } 
-                  }
-            }
-
-            if ($row[4] > 0) {
-                  #find the total number of skycell and the number of skycell with a bad quality flag as well as the full publish state 
-                  $query3 = "     SELECT count(diffSkyfile.quality) AS Ndiff,count(if(diffSkyfile.quality > 0,1,NULL)) AS Ndiffbad,count(if(publishRun.state LIKE 'full',1,NULL)) AS Npub FROM ";
-                  $query3 .= "           diffRun JOIN diffSkyfile USING (diff_id) LEFT JOIN publishRun ON (diffRun.diff_id = stage_id)";
-                  $query3 .= "           WHERE diff_id='$row[0]' GROUP BY diffRun.diff_id";
-                  $qry3 = $projectdb->query($query3);
-
-                  while ($qry3->fetchInto($row3)) {
-                      #if all skycells have bad quality, this is a bad diff. If all the skycells are in a full state in publishRun, it is fully published 
-                      if ($row3[1]==$row3[0]) {
-                          echo "<tr>";
-
-                          echo "<td>$row[5]</td>";
-                          echo "<td>$row[0]</td>";
-                          echo "<td>$row[1]</td>";
-                          echo "<td>$row[2]</td>";
-                          echo "<td>$row[3]</td>";
-                          echo "<td>$row[4]</td>";
-                          echo "<td>$row[6]</td>";
-
-                          echo "</tr>";
-                       }
-                  }
+    
+        // main query with a placeholder for exp_name
+        $query = "
+            SELECT diff_id, warp1, stack1, warp2, stack2, exp_name, comment
+            FROM diffInputSkyfile
+            JOIN warpRun ON (warp1=warp_id OR warp2=warp_id)
+            JOIN fakeRun USING (fake_id)
+            JOIN camRun USING (cam_id)
+            JOIN camProcessedExp USING (cam_id)
+            JOIN chipRun USING (chip_id)
+            JOIN rawExp USING (exp_id)
+            WHERE exp_name LIKE :expname
+            AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+            AND obs_mode NOT LIKE 'ENGINEERING'
+            AND obs_mode NOT LIKE 'MANUAL'
+            GROUP BY diff_id
+        ";
+    
+        if ($debug) { echo "$query<br>"; }
+    
+        // prepare & execute main query
+        $stmt = $projectdb->prepare($query);
+        $stmt->execute([':expname' => $expname]);
+    
+        // prepare secondary query once, reuse it
+        $query3 = "
+            SELECT
+                COUNT(diffSkyfile.quality) AS Ndiff,
+                COUNT(IF(diffSkyfile.quality > 0,1,NULL)) AS Ndiffbad,
+                COUNT(IF(publishRun.state LIKE 'full',1,NULL)) AS Npub
+            FROM diffRun
+            JOIN diffSkyfile USING (diff_id)
+            LEFT JOIN publishRun ON (diffRun.diff_id = stage_id)
+            WHERE diff_id = :diffid
+            GROUP BY diffRun.diff_id
+        ";
+
+        if ($debug) { echo "$query3<br>"; }
+
+        $stmt3 = $projectdb->prepare($query3);
+    
+        // iterate through main query results
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+    
+            // run the nested query regardless of warp2==0 or >0
+            $stmt3->execute([':diffid' => $row[0]]);
+    
+            while ($row3 = $stmt3->fetch(PDO::FETCH_NUM)) {
+                // if all skycells have bad quality
+                if ($row3[1] == $row3[0]) {
+                    echo "<tr>";
+                    echo "<td>" . htmlspecialchars($row[5]) . "</td>"; // exp_name
+                    echo "<td>" . htmlspecialchars($row[0]) . "</td>"; // diff_id
+                    echo "<td>" . htmlspecialchars($row[1]) . "</td>"; // warp1
+                    echo "<td>" . htmlspecialchars($row[2]) . "</td>"; // stack1
+                    echo "<td>" . htmlspecialchars($row[3]) . "</td>"; // warp2
+                    echo "<td>" . htmlspecialchars($row[4]) . "</td>"; // stack2
+                    echo "<td>" . htmlspecialchars($row[6]) . "</td>"; // comment
+                    echo "</tr>";
+                }
             }
         }
@@ -800,58 +963,77 @@
     #-----------------------------------------
     #check for overridden exposures
-    $query = "SELECT COUNT(comment), comment";
-    $query .= " FROM rawExp";
-    $query .= " JOIN chipRun USING (exp_id)";
-    $query .= " JOIN camRun USING (chip_id)";
-    $query .= " WHERE exp_name like '$expname'";
-    $query .= " AND exp_type = 'OBJECT'";
-    $query .= " AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-    $query .= " GROUP BY comment";
-
-    $qry1 = $projectdb->query($query);
-    $doHeader = 1; 
-
-    while ($qry1->fetchInto($row)) {
+    // first query: count per comment
+    $query1 = "
+        SELECT COUNT(comment) AS cnt, comment
+        FROM rawExp
+        JOIN chipRun USING (exp_id)
+        JOIN camRun USING (chip_id)
+        WHERE exp_name LIKE :expname
+          AND exp_type = 'OBJECT'
+          AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+          AND obs_mode NOT LIKE 'ENGINEERING'
+          AND obs_mode NOT LIKE 'MANUAL'
+        GROUP BY comment
+    ";
+    $stmt1 = $projectdb->prepare($query1);
+    $stmt1->execute([':expname' => $expname]);
+
+    if ($debug) { echo "$query1<br>"; }
+    
+    $doHeader = 1;
+    
+    // loop over comments with duplicates
+    while ($row = $stmt1->fetch(PDO::FETCH_NUM)) {
         if ($row[0] > 1) {
-            $query = "SELECT exp_name,dateobs, comment";
-            $query .= " FROM rawExp";
-            $query .= " JOIN chipRun USING (exp_id)";
-            $query .= " JOIN camRun USING (chip_id)";
-            $query .= " WHERE exp_name like '$expname'";
-            $query .= " AND comment LIKE '$row[1]'";
-            $query .= " AND exp_type = 'OBJECT'";
-            $query .= " AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%') AND obs_mode NOT LIKE 'ENGINEERING' AND obs_mode NOT LIKE 'MANUAL'";
-            $query .= " ORDER BY dateobs DESC";
-            $qry2 = $projectdb->query($query);
+            // second query: get exposures with this comment
+            $query2 = "
+                SELECT exp_name, dateobs, comment
+                FROM rawExp
+                JOIN chipRun USING (exp_id)
+                JOIN camRun USING (chip_id)
+                WHERE exp_name LIKE :expname
+                  AND comment LIKE :comment
+                  AND exp_type = 'OBJECT'
+                  AND (obs_mode LIKE '%SS%' OR obs_mode LIKE '%BRIGHT%')
+                  AND obs_mode NOT LIKE 'ENGINEERING'
+                  AND obs_mode NOT LIKE 'MANUAL'
+                ORDER BY dateobs DESC
+            ";
+            $stmt2 = $projectdb->prepare($query2);
+            $stmt2->execute([
+                ':expname' => $expname,
+                ':comment' => $row[1]
+            ]);
+    
+            if ($debug) { echo "$query2<br>"; }
 
             $doskip = 0;
-            while ($qry2->fetchInto($row2)) {
-               if ($doskip) {
+            while ($row2 = $stmt2->fetch(PDO::FETCH_NUM)) {
+                if ($doskip) {
                     if ($doHeader) {
-                       // set up another table
-                       global $table;
-                       echo $table;
-                       echo "<tr>";
-                       createTableTitle("Overridden exposures", 3);
-                       echo "<tr>";
-                       createTableColumnHeader("exp_name");
-                       createTableColumnHeader("dateobs");
-                       createTableColumnHeader("comment");
-                       $doHeader = 0;
+                        // set up another table
+                        global $table;
+                        echo $table;
+                        echo "<tr>";
+                        createTableTitle("Overridden exposures", 3);
+                        echo "<tr>";
+                        createTableColumnHeader("exp_name");
+                        createTableColumnHeader("dateobs");
+                        createTableColumnHeader("comment");
+                        $doHeader = 0;
                     }
+    
                     echo "<tr>";
-
-                    echo "<td>$row2[0]</td>";
-                    echo "<td>$row2[1]</td>";
-                    echo "<td>$row2[2]</td>";
-
+                    echo "<td>" . htmlspecialchars($row2[0]) . "</td>";
+                    echo "<td>" . htmlspecialchars($row2[1]) . "</td>";
+                    echo "<td>" . htmlspecialchars($row2[2]) . "</td>";
                     echo "</tr>";
                 }
-                $doskip = 1;
+                $doskip = 1; // skip the first row of each comment group
             }
         }
     }
+    
     if (!$doHeader) { print "\n"; }
-
 
     // End timer
@@ -870,18 +1052,37 @@
 #
 ###########################################################################
-function getLabels($db, $proj, $server) {
-
-    # order by descending priority as set in project database
-    $sql = "SELECT label FROM science_labels WHERE server LIKE '$server' AND telescope LIKE '$proj' ORDER BY priority DESC, label";
-    if ($debug) {echo "$sql<br>";}
-
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    while ($qry->fetchInto($row)) {
-
-        $labels[] = $row[0];
-    }
-
-    return $labels;
+function getLabels(PDO $db, string $proj, string $server, bool $useLike = false, bool $debug = false): array {
+    // Use equality unless $useLike is true (then add wildcards)
+    if ($useLike) {
+        $sql = "SELECT label FROM science_labels
+                WHERE server LIKE :server AND telescope LIKE :proj
+                ORDER BY priority DESC, label";
+        // add wildcards only if caller didn't already include them
+        $serverParam = (strpos($server, '%') === false && strpos($server, '_') === false) ? "%{$server}%" : $server;
+        $projParam   = (strpos($proj, '%') === false && strpos($proj, '_') === false)   ? "%{$proj}%"   : $proj;
+    } else {
+        $sql = "SELECT label FROM science_labels
+                WHERE server = :server AND telescope = :proj
+                ORDER BY priority DESC, label";
+        $serverParam = $server;
+        $projParam   = $proj;
+    }
+
+    if ($debug) { echo htmlspecialchars($sql) . "<br>\n"; }
+
+    try {
+        $stmt = $db->prepare($sql);
+        $stmt->execute([':server' => $serverParam, ':proj' => $projParam]);
+
+        // fetch all values from the first (and only) column
+        $labels = $stmt->fetchAll(PDO::FETCH_COLUMN);
+        return $labels ?: []; // ensure an array is always returned
+    } catch (PDOException $e) {
+        // handle/log error in a way that suits your app
+        if ($debug) {
+            echo "<b>DB error:</b> " . htmlspecialchars($e->getMessage()) . "<br>\n";
+        }
+        return []; // fail gracefully
+    }
 }
 
@@ -982,29 +1183,29 @@
 
         foreach ($stages as &$stage) {
+	  global $addSearchState; // add this line if inside a function
           // Note: Do not include the state for chip, cam, and warp so that both new and update faults are listed
-      $myTable    = $mainTable[$stage];
-      $myFailures = $getFailures[$stage];
+          $myTable    = $mainTable[$stage];
+          $myFailures = $getFailures[$stage];
           # echo "myTable: $myTable";
 
-      if ("$myFailures" == "NONE") {
+          if ("$myFailures" == "NONE") {
             $link = $defaultlink;
           } else {
             $link = $myFailures . "?pass=" . $pass . "&proj=" . $proj . "&" . $myTable . ".label=" . $thisLabel;
-        if ($addSearchState[$stage]) {
-              // Note: Do not include the state for chip, cam, and warp so that both new and update faults are listed
-              $link .= "&" . $myTable . ".state=" . $searchState;
-            }
+              if ($addSearchState[$stage]) {
+                // Note: Do not include the state for chip, cam, and warp so that both new and update faults are listed
+                $link .= "&" . $myTable . ".state=" . $searchState;
+              }
           }
-      getStateAndFaults($db, $thisLabel, $proj, $selectedState, $stage, $str, $anyFaults);
-      //echo "getStateAndFaults $db, $thisLabel, $proj, $selectedState, $stage, $str, $anyFaults<br>";
-
-      $cellcolor = 0;
-      if ("$stage" == "dist") { 
-        $cellcolor = (!$isUpdate && !$distributing) ? "gray" : 0;
-          }
-          if ("$stage" == "pub") {
-        $cellcolor = (!$isUpdate && !$publishing) ? "gray" : 0;
-          }
-          createFormattedTableCell($anyFaults, $link, $str, $anyFaults, $cellcolor);
+          getStateAndFaults($db, $thisLabel, $proj, $selectedState, $stage, $str, $anyFaults);
+
+          $cellcolor = 0;
+          if ("$stage" == "dist") { 
+            $cellcolor = (!$isUpdate && !$distributing) ? "gray" : 0;
+              }
+              if ("$stage" == "pub") {
+            $cellcolor = (!$isUpdate && !$publishing) ? "gray" : 0;
+              }
+              createFormattedTableCell($anyFaults, $link, $str, $anyFaults, $cellcolor);
         }
         echo "</tr>\n";
@@ -1099,5 +1300,5 @@
 #
 ###########################################################################
-function getNightlyScienceStatus($db, $proj) {
+function getNightlyScienceStatus_old($db, $proj) {
 
     $sql = "SELECT status FROM science_nightlyscience WHERE telescope LIKE '$proj'";
@@ -1109,4 +1310,12 @@
 
     return $row[0];
+}
+
+function getNightlyScienceStatus(PDO $db, $proj, $debug = false) {
+    $sql = "SELECT status FROM science_nightlyscience WHERE telescope LIKE :proj";
+    $stmt = $db->prepare($sql);
+    $stmt->execute([':proj' => "%$proj%"]); // add wildcards
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    return $row ? $row['status'] : '';
 }
 
@@ -1132,47 +1341,67 @@
 ###########################################################################
 function getRevertStatus($db, $proj, $stage) {
-
+    // Note: The $db parameter is now a PDO object.
+
+    // Use a prepared statement to prevent SQL injection.
+    $sql = "SELECT reverting FROM science_reverts WHERE stage LIKE ? AND telescope LIKE ?";
+
+    try {
+        $stmt = $db->prepare($sql);
+        $stmt->execute([$stage, $proj]);
+
+        // Fetch a single column from the next row
+        return $stmt->fetchColumn();
+
+    } catch (PDOException $e) {
+        // Handle database errors
+        error_log("Database error in getRevertStatus: " . $e->getMessage());
+        return null; // Return a safe default value
+    }
+}
+
+###########################################################################
+#
+# Returns state and fault-count (if new) as a string
+#
+###########################################################################
+function getStateAndFaults($db, $label, $proj, $state, $stage, &$str, &$anyFaults) {
     $anyFaults = false;
 
-    $sql = "SELECT reverting FROM science_reverts WHERE stage LIKE '$stage' AND telescope LIKE '$proj'";
-    if ($debug) {echo "$sql<br>";}
-
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $qry->fetchInto($row);
-
-    return $row[0];
-}
-
-###########################################################################
-#
-# Returns state and fault-count (if new) as a string
-#
-###########################################################################
-function getStateAndFaults($db, $label, $proj, $state, $stage, &$str, &$anyFaults) {
-
-    $anyFaults = false;
-    $sql = "SELECT pending, faults FROM $stage WHERE label LIKE '$label' AND dbname LIKE '$proj' ORDER BY timestamp DESC LIMIT 1";
-    if ($debug) {echo "$sql<br>";}
-
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $qry->fetchInto($row);
-
-    $pending = $row[0];
-    $faults = $row[1];
-
-    if ($pending == 0)
-        $str = "";
-    else
-        $str = "$pending";
-
-    if ($state == "new") {
-
-        if ($faults > 0) {
-
-            $str = $str."(".$faults.")";
-            $anyFaults = true;
+    // Use a prepared statement to prevent SQL injection.
+    $sql = "SELECT pending, faults FROM $stage WHERE label LIKE ? AND dbname LIKE ? ORDER BY timestamp DESC LIMIT 1";
+
+    try {
+        $stmt = $db->prepare($sql);
+        $stmt->execute([$label, $proj]);
+
+        // Fetch the result as a numerically indexed array
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+
+        if ($row) {
+            $pending = $row[0];
+            $faults = $row[1];
+
+            if ($pending == 0) {
+                $str = "";
+            } else {
+                $str = "$pending";
+            }
+
+            if ($state == "new" && $faults > 0) {
+                $str .= "($faults)";
+                $anyFaults = true;
+            }
+        } else {
+            // Handle case where no row is found
+            $pending = null;
+            $faults = null;
         }
+
+    } catch (PDOException $e) {
+        // Handle database errors
+        error_log("Database error in getStateAndFaults: " . $e->getMessage());
+        $pending = null;
+        $faults = null;
+        // Optionally, you can set a default message for $str and $anyFaults
     }
 }
@@ -1210,5 +1439,4 @@
 }
 
-
 ###########################################################################
 #
@@ -1217,18 +1445,27 @@
 ###########################################################################
 function createServerDateRow($db, $proj, $server) {
-
-    $sql = "SELECT date FROM science_server_dates WHERE server LIKE '$server' AND telescope LIKE '$proj' ORDER BY date";
-    if ($debug) {echo "$sql<br>";}
-
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $dates = ""; 
-    $firstIn = true;
-    while ($qry->fetchInto($row)) {
-        if ($firstIn) {
-            $dates = $row[0];
-            $firstIn = false;
+    // Note: The $db parameter is now a PDO object.
+
+    // Use a prepared statement to prevent SQL injection.
+    $sql = "SELECT date FROM science_server_dates WHERE server LIKE ? AND telescope LIKE ? ORDER BY date";
+
+    try {
+        $stmt = $db->prepare($sql);
+        $stmt->execute([$server, $proj]);
+
+        $dates = "";
+        $firstIn = true;
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            if ($firstIn) {
+                $dates = $row[0];
+                $firstIn = false;
+            } else {
+                $dates .= ", " . $row[0];
+            }
         }
-        else $dates = $dates . ", " . $row[0];
+    } catch (PDOException $e) {
+        // Handle database errors
+        error_log("Database error in createServerDateRow: " . $e->getMessage());
+        $dates = "DB ERROR";
     }
 
@@ -1237,5 +1474,4 @@
     echo "<td>$dates</td>";
     echo "</tr>";
-
 }
 
@@ -1333,17 +1569,38 @@
 #
 ###########################################################################
-function getServerStatus($db,$proj, $server, &$alive, &$running, &$host) {
-
-#    $sql = "SELECT alive, running FROM science_servers WHERE server LIKE '$server' AND telescope LIKE '$proj' ORDER BY timestamp DESC LIMIT 1";
-    $sql = "SELECT alive, running, host FROM live_servers WHERE server LIKE '$server' AND telescope LIKE '$proj' ORDER BY timestamp DESC LIMIT 1";
-    if($debug){echo "$sql<br>";}
-
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $qry->fetchInto($row);
-
-    $alive = $row[0];
-    $running = $row[1];
-    $host = $row[2];
+
+function getServerStatus($db, $proj, $server, &$alive, &$running, &$host) {
+    // Note: The $db parameter is now expected to be a PDO object.
+
+    // Use a prepared statement to prevent SQL injection.
+    $sql = "SELECT alive, running, host FROM live_servers WHERE server LIKE ? AND telescope LIKE ? ORDER BY timestamp DESC LIMIT 1";
+
+    try {
+        $stmt = $db->prepare($sql);
+
+        // Bind parameters and execute the query
+        $stmt->execute([$server, $proj]);
+        
+        // Fetch the result as a numerically indexed array, matching the old behavior
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+
+        if ($row) {
+            $alive = $row[0];
+            $running = $row[1];
+            $host = $row[2];
+        } else {
+            // Handle case where no data is found for the server
+            $alive = null;
+            $running = null;
+            $host = null;
+        }
+
+    } catch (PDOException $e) {
+        // Handle database errors
+        error_log("Database error in getServerStatus: " . $e->getMessage());
+        $alive = null;
+        $running = null;
+        $host = null;
+    }
 }
 
@@ -1353,15 +1610,20 @@
 #
 ###########################################################################
-function getLastUpdateTime($db) {
-
-    $anyFaults = false;
+function getLastUpdateTime(PDO $db) {
     $sql = "SELECT timestamp FROM chip ORDER BY timestamp DESC LIMIT 1";
-    if ($debug) {echo "$sql<br>";}
-
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $qry->fetchInto($row);
-
-    return $row[0];
+    
+    // run the query
+    $stmt = $db->query($sql);
+    if (!$stmt) {
+        // if query fails, print error info
+        $errorInfo = $db->errorInfo();
+        echo "<b>Error with $sql:</b> " . $errorInfo[2] . "<br>\n";
+        return null;
+    }
+
+    // fetch the first row as numeric array
+    $row = $stmt->fetch(PDO::FETCH_NUM); 
+
+    return $row ? $row[0] : null; // return timestamp or null
 }
 
@@ -1391,29 +1653,26 @@
 ###########################################################################
 function showReplicationsStatus($replHost, $replUser, $replPassword, $replDatabaseName) {
-    //print "<br>$replHost, $replUser, $replPassword, $replDatabaseName<br/>";
     global $czardb;
 
-    // Prepare storage info early, https://panstarrs.atlassian.net/browse/IPP-2338
     $storageInfo = '';
     $hostPrefixParts = explode(' ', $replDatabaseName);
-    $hostPrefix = addslashes($hostPrefixParts[0]);
-
-    $sql = sprintf(
-        "SELECT host, FORMAT(total,1), FORMAT(available,2), FORMAT(used,2), writable, readable, FORMAT(used/total*100,1)
-         FROM hosts
-         WHERE host LIKE '%s%%' order by available desc
-         LIMIT 1",
-         $hostPrefix
-    );
-
-    $qry = $czardb->query($sql);
-    if (PEAR::isError($qry)) {
-        $storageInfo = "<td style=\"background-color: red; color: white;\">Storage query error</td>";
-    } else {
-        if ($qry->fetchInto($row)) {
+    $hostPrefix = $hostPrefixParts[0];
+
+    // Using a prepared statement with a placeholder for security
+    $sql = "SELECT host, FORMAT(total,1), FORMAT(available,2), FORMAT(used,2), writable, readable, FORMAT(used/total*100,1)
+            FROM hosts
+            WHERE host LIKE ?
+            ORDER BY available DESC
+            LIMIT 1";
+
+    try {
+        $stmt = $czardb->prepare($sql);
+        $stmt->execute(["$hostPrefix%"]);
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+
+        if ($row) {
             list($host, $total, $available, $used, $writable, $readable, $ratioStr) = $row;
-            $ratio = floatval($ratioStr);  // convert to number
-    
-            // Determine color
+            $ratio = floatval($ratioStr);
+
             if ($ratio < 80) {
                 $color = 'lightgreen';
@@ -1423,31 +1682,40 @@
                 $color = 'pink';
             }
-    
+
             $storageInfo = "<td style=\"background-color: $color;\">$ratioStr% of $total</td>";
         } else {
             $storageInfo = "<td>No host storage data</td>";
         }
-    }
-
-    $dbRepl = DB::connect("mysql://$replUser:$replPassword@$replHost");
-    if (PEAR::isError($dbRepl)) {
+    } catch (PDOException $e) {
+        $storageInfo = "<td style=\"background-color: red; color: white;\">Storage query error</td>";
+    }
+
+    // Connect to the replication database using PDO
+    try {
+        $dbRepl = new PDO("mysql:host=$replHost;", $replUser, $replPassword);
+        $dbRepl->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+    } catch (PDOException $e) {
         echo "<tr><td>$replDatabaseName</td>$storageInfo<td style=\"background-color: pink;\">MySQL DB connection error - check configuration</td></tr>";
-        return -1; // Don't stop the rest of the script
+        return -1;
     }
 
     // Query replication status
-    $res = $dbRepl->query('SHOW SLAVE STATUS');
-    if (PEAR::isError($res)) {
+    try {
+        $res = $dbRepl->query('SHOW SLAVE STATUS');
+        $row = $res->fetch(PDO::FETCH_NUM);
+    } catch (PDOException $e) {
         echo "<tr><td>$replDatabaseName</td>$storageInfo<td style=\"background-color: pink;\">MySQL query error - invalid user or privileges</td></tr>";
-        $dbRepl->disconnect();
+        $dbRepl = null;
         return -1;
     }
 
-    if (!$res->fetchInto($row)) {
+    if (!$row) {
         echo "<tr><td>$replDatabaseName</td>$storageInfo<td style=\"background-color: pink;\">No replication data available</td></tr>";
-        $dbRepl->disconnect();
+        $dbRepl = null;
         return 0;
     }
 
+    // Note: The index for 'Seconds_Behind_Master' and 'Last_IO_Error' might vary based on MySQL version.
+    // It's safer to use PDO::FETCH_ASSOC and the column names.
     $errorStatusInMySql = $row[18];
     $sec_behind = $row[32];
@@ -1463,5 +1731,4 @@
 
     echo "<tr><td>$replDatabaseName</td>$storageInfo";
-
     echo "<td>$replStatus</td></tr>";
 
@@ -1471,8 +1738,6 @@
     }
 
-    $dbRepl->disconnect();
-
-}
-
+    $dbRepl = null; // Close connection
+}
 ###########################################################################
 #
@@ -1481,28 +1746,24 @@
 ###########################################################################
 function showDatabaseStatus($Host, $User, $Password, $DatabaseName) {
-    //print "<br>$Host, $User, $Password, $DatabaseName<br/>";
     global $czardb;
-    // Prepare storage info early, https://panstarrs.atlassian.net/browse/IPP-2338
     $storageInfo = '';
     $hostPrefixParts = explode(' ', $DatabaseName);
     $hostPrefix = addslashes($hostPrefixParts[0]);
 
-    $sql = sprintf(
-        "SELECT host, FORMAT(total,1), FORMAT(available,2), FORMAT(used,2), writable, readable, FORMAT(used/total*100,1)
-         FROM hosts
-         WHERE host LIKE '%s%%' order by available desc
-         LIMIT 1",
-         $hostPrefix
-    );
-
-    $qry = $czardb->query($sql);
-    if (PEAR::isError($qry)) {
-        $storageInfo = "<td style=\"background-color: red; color: white;\">Storage query error</td>";
-    } else {
-        if ($qry->fetchInto($row)) {
+    $sql = "SELECT host, FORMAT(total,1), FORMAT(available,2), FORMAT(used,2), writable, readable, FORMAT(used/total*100,1)
+            FROM hosts
+            WHERE host LIKE ?
+            ORDER BY available DESC
+            LIMIT 1";
+
+    try {
+        $stmt = $czardb->prepare($sql);
+        $stmt->execute(["$hostPrefix%"]);
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+
+        if ($row) {
             list($host, $total, $available, $used, $writable, $readable, $ratioStr) = $row;
-            $ratio = floatval($ratioStr);  // convert to number
-    
-            // Determine color
+            $ratio = floatval($ratioStr);
+
             if ($ratio < 80) {
                 $color = 'lightgreen';
@@ -1512,20 +1773,25 @@
                 $color = 'pink';
             }
-    
+
             $storageInfo = "<td style=\"background-color: $color;\">$ratioStr% of $total</td>";
         } else {
             $storageInfo = "<td>No host storage data</td>";
         }
-    }
-
-    $db = DB::connect("mysql://$User:$Password@$Host");
-    if (PEAR::isError($db)) {
+    } catch (PDOException $e) {
+        $storageInfo = "<td style=\"background-color: red; color: white;\">Storage query error</td>";
+    }
+
+    try {
+        $db = new PDO("mysql:host=$Host;", $User, $Password);
+        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+        $Status = "OK";
+    } catch (PDOException $e) {
+        $Status = "<font style=\"BACKGROUND-COLOR: yellow\" color=\"red\">PROBLEM</font>";
         echo "<tr><td>$DatabaseName</td>$storageInfo<td>$Status</td>";
         return -1;
     }
-    $Status = ($errorStatusInMySql==0?"OK":"<font style=\"BACKGROUND-COLOR: yellow\"  color=\"red\">PROBLEM</font>");
+
     echo "<tr><td>$DatabaseName</td>$storageInfo<td>$Status</td>";
-
-    $db->disconnect();
+    $db = null; // Close connection
 }
 
@@ -1571,20 +1837,17 @@
 #
 ###########################################################################
-function createSummitDataTable2($projectdb, $pass, $proj, $debug) {
-
+function createSummitDataTable($projectdb, $pass, $proj, $debug) {
     // timer start
     $start = microtime(true);
 
     // Define constants
-    $mjdDay = getMJD();
-    $expname = "%{$mjdDay}%";
-
+    $mjdDay   = getMJD();
+    $expname  = "%{$mjdDay}%";
     $mjdDaym1 = $mjdDay - 1;
     $expnamem1 = "%{$mjdDaym1}%";
-
     $mjdDaym5 = $mjdDay - 5;
     $expnamem5 = "o{$mjdDaym5}%";
 
-    $currentDate = gmdate("Y-m-d H:i:s");
+    $currentDate  = gmdate("Y-m-d H:i:s");
     $previousDate = gmdate("Y-m-d", strtotime("-1 day"));
 
@@ -1597,789 +1860,97 @@
     echo "<tr>";
       createTableColumnHeader("Exposure type");
-      createTableColumnHeader("<a href=\"czartool_labels.php?pass=".$pass."&proj=".$proj."&label=all&stage=summitExp\"><font color=\"blue\">summitExp status</font></a>");
-      createTableColumnHeader("<a href=\"czartool_labels.php?pass=".$pass."&proj=".$proj."&label=all&stage=downloadExp\"><font color=\"blue\">downloadExp status</font></a>");
-      createTableColumnHeader("<a href=\"czartool_labels.php?pass=".$pass."&proj=".$proj."&label=all&stage=rawExp\"><font color=\"blue\">rawExp </a>/
-                               <a href=\"czartool_labels.php?pass=".$pass."&proj=".$proj."&label=all&stage=newExp\"><font color=\"blue\"> newExp</font></a>");
-
+      createTableColumnHeader("<a href=\"czartool_labels.php?pass=$pass&proj=$proj&label=all&stage=summitExp\"><font color=\"blue\">summitExp status</font></a>");
+      createTableColumnHeader("<a href=\"czartool_labels.php?pass=$pass&proj=$proj&label=all&stage=downloadExp\"><font color=\"blue\">downloadExp status</font></a>");
+      createTableColumnHeader("<a href=\"czartool_labels.php?pass=$pass&proj=$proj&label=all&stage=rawExp\"><font color=\"blue\">rawExp</font></a> /
+                               <a href=\"czartool_labels.php?pass=$pass&proj=$proj&label=all&stage=newExp\"><font color=\"blue\">newExp</font></a>");
+
+    // --- Query 1 ---
     $sql = "
-        SELECT sum_type, pzstate, rawstate, count(*), sum_exp, pz_exp, raw_exp 
-        FROM 
-          (SELECT summitExp.exp_type as sum_type, pzDownloadExp.state as pzstate, rawExp.state as rawstate, summitExp.exp_name as sum_exp, pzDownloadExp.exp_name as pz_exp, rawExp.exp_name as raw_exp
-           FROM summitExp
-           LEFT JOIN pzDownloadExp USING (summit_id)
-           LEFT JOIN rawExp ON summitExp.exp_name = rawExp.exp_name
-           WHERE summitExp.summit_id >= (
-               SELECT COALESCE(MIN(summit_id), 0) 
-               FROM summitExp 
-               WHERE exp_name LIKE '$expnamem5'
-            ) 
-            AND summitExp.exp_name LIKE '$expname' ORDER BY summitExp.dateobs desc) as sub 
+        SELECT sum_type, pzstate, rawstate, count(*), sum_exp, pz_exp, raw_exp
+        FROM (SELECT summitExp.exp_type as sum_type,
+                     pzDownloadExp.state as pzstate,
+                     rawExp.state as rawstate,
+                     summitExp.exp_name as sum_exp,
+                     pzDownloadExp.exp_name as pz_exp,
+                     rawExp.exp_name as raw_exp
+              FROM summitExp
+              LEFT JOIN pzDownloadExp USING (summit_id)
+              LEFT JOIN rawExp ON summitExp.exp_name = rawExp.exp_name
+              WHERE summitExp.summit_id >= (
+                    SELECT COALESCE(MIN(summit_id), 0)
+                    FROM summitExp WHERE exp_name LIKE '$expnamem5')
+              AND summitExp.exp_name LIKE '$expname'
+              ORDER BY summitExp.dateobs desc) as sub
         GROUP BY sum_type, pzstate, rawstate";
     if ($debug) {echo "$sql<br>";}
 
-
     $qry = $projectdb->query($sql);
-    while ($qry->fetchInto($tmp)) {
+    while ($tmp = $qry->fetch(PDO::FETCH_NUM)) {
         list($expType, $downloadState, $rawState, $count, $summit_exp, $pz_exp, $raw_exp) = $tmp;
-        if ($proj == "gpc1") {
-            $summit_exp_link = "<a href=\"https://svn.ifa.hawaii.edu/summit/ds/gpc1/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>";
+        $summit_exp_link = ($proj == "gpc1")
+            ? "<a href=\"https://svn.ifa.hawaii.edu/summit/ds/gpc1/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>"
+            : "<a href=\"https://svn.ifa.hawaii.edu/conductorb/ds/gpc2/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>";
+
+        if ($downloadState === NULL) {
+            echo "<tr><td>$summit_exp_link $expType <td>loading imfiles <td>null <td bgcolor=\"yellow\"><font color=\"red\">$count";
+        } elseif ($downloadState === 'run') {
+            echo "<tr><td>$summit_exp_link $expType <td>$pz_exp copying <td>$pz_exp saving <td bgcolor=$columnHeaderColor><font color=\"red\">$count";
         } else {
-            $summit_exp_link = "<a href=\"https://svn.ifa.hawaii.edu/conductorb/ds/gpc2/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>";
+            if ($rawState === NULL) {
+                echo "<tr><td>$summit_exp_link $expType <td>$pz_exp $downloadState <td>$pz_exp registering <td bgcolor=$columnHeaderColor><font color=\"blue\">$count";
+            } else {
+                echo "<tr><td>$summit_exp_link $expType <td>$pz_exp $downloadState <td>$raw_exp $rawState <td>$count";
+            }
         }
-        if ($downloadState == NULL) { 
-          echo  "<tr><td> $summit_exp_link $expType <td> loading imfiles <td> null <td bgcolor=\"yellow\"> <font color=\"red\">$count";
-        } else {
-          if ($downloadState == 'run') { 
-            echo  "<tr><td> $summit_exp_link $expType <td> $pz_exp copying <td> $pz_exp saving <td bgcolor=$columnHeaderColor> <font color=\"red\">$count"; 
-          } else {
-            if ($rawState == NULL) { 
-              echo  "<tr><td> $summit_exp_link $expType <td> $pz_exp $downloadState <td> $pz_exp registering <td bgcolor=$columnHeaderColor> <font color=\"blue\">$count";
-            } else {                                    
-              echo  "<tr><td> $summit_exp_link $expType <td> $pz_exp $downloadState <td> $raw_exp $rawState <td> $count";
-            }
-          }
-        }
-    }
-
+    }
+
+    // --- Query 2 ---
     echo "<tr><th colspan=4>Last night $previousDate (UTC), $mjdDaym1 (MJD)";
     $sql = "
-        SELECT sum_type, pzstate, rawstate, count(*), sum_exp, pz_exp, raw_exp 
-        FROM 
-          (SELECT summitExp.exp_type as sum_type, pzDownloadExp.state as pzstate, rawExp.state as rawstate, summitExp.exp_name as sum_exp, pzDownloadExp.exp_name as pz_exp, rawExp.exp_name as raw_exp
-           FROM summitExp
-           LEFT JOIN pzDownloadExp USING (summit_id)
-           LEFT JOIN rawExp ON summitExp.exp_name = rawExp.exp_name
-           WHERE summitExp.summit_id >= (
-               SELECT COALESCE(MIN(summit_id), 0) 
-               FROM summitExp 
-               WHERE exp_name LIKE '$expnamem5'
-            ) 
-            AND summitExp.exp_name LIKE '$expnamem1' ORDER BY summitExp.dateobs desc) as sub 
+        SELECT sum_type, pzstate, rawstate, count(*), sum_exp, pz_exp, raw_exp
+        FROM (SELECT summitExp.exp_type as sum_type,
+                     pzDownloadExp.state as pzstate,
+                     rawExp.state as rawstate,
+                     summitExp.exp_name as sum_exp,
+                     pzDownloadExp.exp_name as pz_exp,
+                     rawExp.exp_name as raw_exp
+              FROM summitExp
+              LEFT JOIN pzDownloadExp USING (summit_id)
+              LEFT JOIN rawExp ON summitExp.exp_name = rawExp.exp_name
+              WHERE summitExp.summit_id >= (
+                    SELECT COALESCE(MIN(summit_id), 0)
+                    FROM summitExp WHERE exp_name LIKE '$expnamem5')
+              AND summitExp.exp_name LIKE '$expnamem1'
+              ORDER BY summitExp.dateobs desc) as sub
         GROUP BY sum_type, pzstate, rawstate";
     if ($debug) {echo "$sql<br>";}
 
     $qry = $projectdb->query($sql);
-    while ($qry->fetchInto($tmp)) {
+    while ($tmp = $qry->fetch(PDO::FETCH_NUM)) {
         list($expType, $downloadState, $rawState, $count, $summit_exp, $pz_exp, $raw_exp) = $tmp;
-        if ($proj == "gpc1") {
-            $summit_exp_link = "<a href=\"https://svn.ifa.hawaii.edu/summit/ds/gpc1/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>";
+        $summit_exp_link = ($proj == "gpc1")
+            ? "<a href=\"https://svn.ifa.hawaii.edu/summit/ds/gpc1/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>"
+            : "<a href=\"https://svn.ifa.hawaii.edu/conductorb/ds/gpc2/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>";
+
+        if ($downloadState === NULL) {
+            echo "<tr><td>$summit_exp_link $expType <td>loading imfiles <td>null <td bgcolor=\"yellow\"><font color=\"red\">$count";
+        } elseif ($downloadState === 'run') {
+            echo "<tr><td>$summit_exp_link $expType <td>$pz_exp copying <td>$pz_exp saving <td bgcolor=$columnHeaderColor><font color=\"red\">$count";
         } else {
-            $summit_exp_link = "<a href=\"https://svn.ifa.hawaii.edu/conductorb/ds/gpc2/$summit_exp/\" target='_blank'><font color=\"blue\">$summit_exp</font></a>";
+            if ($rawState === NULL) {
+                echo "<tr><td>$summit_exp_link $expType <td>$pz_exp $downloadState <td>$pz_exp registering <td bgcolor=$columnHeaderColor><font color=\"blue\">$count";
+            } else {
+                echo "<tr><td>$summit_exp_link $expType <td>$pz_exp $downloadState <td>$raw_exp $rawState <td>$count";
+            }
         }
-        if ($downloadState == NULL) { 
-          echo  "<tr><td> $summit_exp_link $expType <td> loading imfiles <td> null <td bgcolor=\"yellow\"> <font color=\"red\">$count";
-        } else {
-          if ($downloadState == 'run') { 
-            echo  "<tr><td> $summit_exp_link $expType <td> $pz_exp copying <td> $pz_exp saving <td bgcolor=$columnHeaderColor> <font color=\"red\">$count"; 
-          } else {
-            if ($rawState == NULL) { 
-              echo  "<tr><td> $summit_exp_link $expType <td> $pz_exp $downloadState <td> $pz_exp registering <td bgcolor=$columnHeaderColor> <font color=\"blue\">$count";
-            } else {                                    
-              echo  "<tr><td> $summit_exp_link $expType <td> $pz_exp $downloadState <td> $raw_exp $rawState <td> $count";
-            }
-          }
-        }
-    }
-
+    }
+
+    // End timer
     echo "<tr><th colspan=4>";
-    // End timer
     $total_time = round(microtime(true) - $start, 3);
     echo "<center>loading in $total_time seconds<br></center>";
-
     echo "</th></tr>";
     echo "</table><br>";
 }
-
-function formatState($state) {
-    // Formats display of the state for better readability
-    if (is_null($state)) {
-        return "<span style='color: red;'>null</span>";
-    } elseif ($state == 'run') {
-        return "<span style='color: red;'>loading</span>";
-    } else {
-        return $state;
-    }
-}
-###########################################################################
-#
-# Gets disk status
-#
-###########################################################################
-function getHostsold($db) {
-    // timer start
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $start = $time;
-
-    #echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>";
-    echo "<script type=\"text/javascript\" src=\"loader.js\"></script>";
-    echo "<br><div class=\"chartWithOverlay\" style=\"position: relative; width: 640px\">"; 
-    echo "  <div id=\"disk_div\" style=\"width:640px; height:5000px\"></div>";
-    echo "  <div class=\"overlay\" style=\"position: absolute; width: 100px; top: 35px; right: 20px;\">";
-    echo "    <div style=\"font-size: 12px; height: 14px; border: 1px solid; background-color: #00ff00\"><center>Free</div>";
-    echo "    <div style=\"font-size: 12px; height: 14px; border: 1px solid; background-color: #ffff00\"><center>Used</div>";
-    echo "    <div style=\"font-size: 12px; height: 14px; border: 1px solid; background-color: #ff0000\"><center>> 97% limit</div>";
-    echo "    <div style=\"font-size: 12px; height: 14px; border: 1px solid; background-color: #bbbbbb\"><center>Down | Repair</div>";
-    echo "    <div style=\"font-size: 12px; height: 14px; border: 1px solid; background-color: #ffffff\"><center>Power Off</div>";
-    echo "  </div>";
-    echo "</div>";
-
-    echo "<script type=\"text/javascript\">";
-    echo "google.charts.load('current', {packages: ['corechart', 'bar']});";
-    echo "google.charts.setOnLoadCallback(drawBarColors);";
-    echo "function drawBarColors() {";
-    echo "  var data = google.visualization.arrayToDataTable([";
-    echo "    ['Host', ";
-    echo "     'used', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}}, ";
-    echo "     'free', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}}],";
-
-    //$sql = "SELECT host, format(total,1), format(available,2), format(used,2), writable, readable, format(used/total*100,2) FROM hosts";
-    $sql = "SELECT host, format(total, 2), format(available, 2), format(used, 2), writable, readable, format(used/total*100,3) as ratio FROM hosts where xattr <> 3 ";
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-
-    // Set environment variables
-    putenv("PATH=/usr/local/bin:/usr/bin:/bin:/data/ippc65.0/ipp/src/psconfig//ipp-20210708-gentoo.lin64/bin");
-    putenv("PERL5LIB=/data/ippc65.0/ipp/src/psconfig//ipp-20210708-gentoo.lin64/lib");
-
-    # not sure why neb-host doesn't work on ipp113 CCL.20210216
-    exec("neb-host |cut -b 17-30,86-", $hostnoteall);
-    while ($qry->fetchInto($row)) {
-        list($host, $total, $available, $used, $writable, $readable, $ratio) = $row;
-       $hostnotes = preg_grep("/$host/", $hostnoteall);
-       $hostnote = array_values($hostnotes);
-       if ($writable == 1 && $readable == 1) {
-          if ($ratio >= 97) {
-            # space used over limits, up mode (read/write ok).
-            echo "[ \"$host:up\", ";
-            echo "      $used, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #ff0000;\", \"<i><b>$host: $ratio% of $total TB used</b></i><br>$hostnote[0]\", ";
-            echo " $available, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #00ff00;\", \"<i><b>$host: $available TB free</b></i>\"],";
-          }
-          else {
-            # enough space, up mode (read/write ok).
-            echo "[ \"$host:up\", ";
-            echo "      $used, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #ffff00;\", \"<i><b>$host: $ratio% of $total TB used</b></i><br>$hostnote[0]\", ";
-            echo " $available, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #00ff00;\", \"<i><b>$host: $available TB free</b></i>\"],";
-         }
-       }
-       else if ($writable == 0 && $readable == 1) {
-          if ($ratio >= 97) {
-            # space used over limits, repair mode (read only).
-            echo "[ \"$host:repair\", ";
-            echo "      $used, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #ff0000;\", \"<i><b>$host: $ratio% of $total TB used<br><font color=red>$hostnote[0]\", ";
-            echo " $available, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #bbbbbb;\", \"<i><b>$host: $available TB free</b></i>\"],";
-          }
-          else {
-            # enough space, but hosts are unstable, repair mode (read only).
-            echo "[ \"$host:repair\", ";
-            echo "      $used, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #ffff00;\", \"<i><b>$host: $ratio% of $total TB used<br>$hostnote[0]\", ";
-            echo " $available, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #bbbbbb;\", \"<i><b>$host: $available TB free</b></i>\"],";
-          }
-       }
-       else {
-            # down mode (can't read or write).
-            echo "[ \"$host:down\", ";
-            echo "      $used, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #bbbbbb;\", \"<i><b>$host: $ratio% used<br>$hostnote[0]\", ";
-            echo " $available, \"stroke-width: 0.3;\"+\"stroke-color: black;\"+\"color: #bbbbbb;\", \"<i><b>$host: $available TB free</b></i>\"],";
-       }
-    }
-   echo "  ]);";
-   echo "   var options = {";
-   echo "     title: 'Nebulous Disk Use across IPP Clusters',";
-   echo "     titleTextStyle: {color: 'black', fontSize: 15},";
-   echo "     width: '100%',";
-   echo "     height: '100%',";
-   echo "     tooltip: {isHtml: true},";
-   echo "     legend: { position: 'none',";
-   echo "               alignment: 'end',";
-   echo "               maxLines: 3},";
-   echo "     hAxis: {  title: 'Space (TB)',";
-   echo "               gridlines: {count: 10}";
-   echo "     },";
-   echo "     vAxis: { textPosition: 'in', direction: 1 },";
-   echo "     bar: { groupWidth: '95%' },";
-   echo "     chartArea: {left:20, top:30, right:20, bottom:40},";
-   echo "     fontSize: 15,";
-   echo "     isStacked: true,";
-   echo "     colors: ['#76A7FA', '#FF5733', '#C70039', '#900C3F', '#581845']";
-   echo "   };";
-   echo "   var chart = new google.visualization.BarChart(document.getElementById('disk_div'));";
-   echo "   chart.draw(data, options);";
-   echo " }";
-   echo "</script>";
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $finish = $time;
-    $total_time = round(($finish - $start), 3);
-    $start= $finish;
-    echo "<center>loading in $total_time seconds<br></center>";
-}
-
-###########################################################################
-#
-# Gets disk status
-#
-###########################################################################
-function getHosts($db) {
-    $start = microtime(true);
-
-
-    // Set environment variables
-    putenv("PATH=/usr/local/bin:/usr/bin:/bin:/data/ippc65.0/ipp/src/psconfig//ipp-20210708-gentoo.lin64/bin");
-    putenv("PERL5LIB=/data/ippc65.0/ipp/src/psconfig//ipp-20210708-gentoo.lin64/lib");
-
-    $hostnoteall = array();
-    exec("neb-host |cut -b 17-30,86-", $hostnoteall);
-
-    $sql = "SELECT host, format(total, 2), format(available, 2), format(used, 2), writable, readable, format(used/total*100,3) as ratio FROM hosts where xattr <> 3 ORDER BY CASE WHEN ROUND(used/total*100,3) = 100 THEN 0 ELSE 1 END, CASE WHEN host LIKE '%\_bck.0' THEN 1 ELSE 0 END, host;";
-    $qry = $db->query($sql);
-
-    if ($qry === false) {
-        echo "<b>Error with SQL query</b><br>";
-        return;
-    }
-
-    $sqlMax = "SELECT MAX(total) AS max_total FROM hosts WHERE xattr <> 3";
-    $qryMax = $db->query($sqlMax);
-    $maxUsed = 0;
-
-    if ($qryMax && $qryMax->fetchInto($rowMax)) {
-        $maxUsed = $rowMax[0];  // max_total value
-    }
-
-    $dataRows = array();
-    while ($qry->fetchInto($row)) {
-        // Access by index
-        $host      = $row[0]; // ipp071.0
-        $total     = $row[1]; // 80.0352 TB
-        $available = $row[2]; // 10.8096 TB
-        $used      = $row[3]; // 69.2256 TB
-        $writable  = $row[4]; // 0
-        $readable  = $row[5]; // 1
-        $ratio     = $row[6]; // 86.4939 %
-
-        // Match notes
-        $hostnotes = preg_grep("/$host/", $hostnoteall);
-        $hostnote = reset($hostnotes);
-
-        // Defaults
-        $styles    = "stroke-width: .5; stroke-color: black;";
-        $status    = "down";
-        $usedColor = "lightgrey";
-        $freeColor = "lightgrey";
-
-        // Status logic
-        if ($writable == 1 && $readable == 1) {
-            $status    = "up";
-            $usedColor = ($ratio >= 97) ? "pink" : "yellow"; // red if â¥97%
-            $freeColor = "#a6ec99"; // green free
-        } elseif ($writable == 0 && $readable == 1) {
-            $status    = "repair";
-            $usedColor = ($ratio >= 97) ? "pink" : "yellow";
-            $freeColor = "lightgrey"; // grey free
-        } elseif ($available < 1) {
-            $used      = $maxUsed;
-            $available = 0;
-            $status    = "offline";
-            $usedColor = "white";
-            $freeColor = "white";
-        }
-
-        if ($status == "down") {
-            $tooltipUsed = $host . ": " . round($ratio, 1) . "% of " . $total . " TB used ".
-                           "<span style='color:red; font-weight:bold;'> <br>" . $hostnote .  "</span>";
-            $tooltipFree = "<span style='color:red; font-weight:bold;'>" .
-                           $host . ": " . $available . " TB free" .
-                           "</span>";
-        } elseif ($status == "offline") {
-            $tooltipUsed = $host . ": " . round($ratio, 1) . "% of " . $total . " TB used ".
-                           "<span style='color:red; font-weight:bold;'> <br>" . $hostnote .  "</span>";
-            $tooltipFree = "<span style='color:red; font-weight:bold;'>" .
-                           $host . ": " . $total . " TB free" .
-                           "</span>";
-        } else {
-            $tooltipUsed = $host . ": " . round($ratio, 1) . "% of " . $total . " TB used<br>" . $hostnote;
-            $tooltipFree = $host . ": " . $available . " TB free";
-        }
-
-        // Build data row (manual string concatenation for old PHP)
-        $dataRows[] =
-            "[ \"" . $host . ":" . $status . "\", " .
-            $used . ", \"" . $styles . " color: " . $usedColor . ";\", \"" . $tooltipUsed . "\", " .
-            $available . ", \"" . $styles . " color: " . $freeColor . ";\", \"" . $tooltipFree . "\" ]";
-
-    }
-    echo "<script type=\"text/javascript\">
-            google.charts.load('current', {packages: ['corechart', 'bar']});
-            google.charts.setOnLoadCallback(drawBarColors);
-
-            function drawBarColors() {
-                var data = google.visualization.arrayToDataTable([
-                    ['Host', 'Used', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}},
-                     'Free', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}}],
-                    " . implode(",\n", $dataRows) . "
-                ]);
-
-                var options = {
-                    title: 'Nebulous Disk Use across IPP Clusters',
-                    titleTextStyle: {color: 'black', fontSize: 15},
-                    width: '100%',
-                    height: '100%',
-                    tooltip: {isHtml: true},
-                    legend: { position: 'none', alignment: 'end', maxLines: 3 },
-                    hAxis: { title: 'Space (TB)', gridlines: {count: 20} },
-                    vAxis: { textStyle: {fontSize: 15}, format: 'string',  textPosition: 'in', direction: 1},
-                    bar: { groupWidth: '95%' },
-                    chartArea: {left:40, top:50, right:20, bottom:40},
-                    fontSize: 15,
-                    allowHtml: true,
-                    isStacked: true
-                };
-
-                var chart = new google.visualization.BarChart(document.getElementById('disk_div'));
-                chart.draw(data, options);
-            }
-          </script>";
-
-echo "<script type='text/javascript' src='loader.js'></script>
-<br>
-<div class='chartWithOverlay' style='position: relative; width: 100%'>
-
-    <div id='disk_div' style='width:100%; height:5000px'></div>
-
-    <div class='overlay' style='position: absolute; width:160px; top:50px; right:30px; font-family:Arial, sans-serif;'>
-
-      <div style='font-size:15px; border:1px solid #888; background-color:lightgreen; color:black; text-align:center; margin-bottom:1px;'>Free</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:yellow    ; color:black; text-align:center; margin-bottom:1px;'>Used</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:pink      ; color:black; text-align:center; margin-bottom:1px;'>97% Usage</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:lightgrey ; color:black; text-align:center; margin-bottom:1px;'>Down/Repair</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:white     ; color:black; text-align:center; margin-bottom:1px;'>Offline</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:#42a5f5   ; color:white; text-align:center; margin-bottom:1px;'>
-        <a href='https://atrcganglia.ifa.hawaii.edu/' target='_blank' style='text-decoration:none; color:white; font-weight:bold;'>ATRC ganglia</a>
-      </div>
-
-    </div>
-
-</div>";
-
-    $total_time = round(microtime(true) - $start, 3);
-    echo "<center>loading in $total_time seconds<br></center>";
-}
-
-###########################################################################
-#
-# Gets disk spaces status
-#
-###########################################################################
-function getSpaces($db) {
-    // timer start
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $start = $time;
-
-
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ippb%' and xattr <> 3")->fetchInto($row);
-    $ippbusable = $row[0];
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ippb%' and writable = 1 and xattr <> 3")->fetchInto($row);
-    $ippbfree = $row[0];
-
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host not like 'ippb%' and xattr <> 3")->fetchInto($row);
-    $ippusable = $row[0];
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host not like 'ippb%' and writable = 1 and xattr <> 3")->fetchInto($row);
-    $ippfree = $row[0];
-
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ipp%_bck.0' and xattr <> 3")->fetchInto($row);
-    $bckusable = $row[0];
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ipp%_bck.0' and writable = 1 and xattr <> 3")->fetchInto($row);
-    $bckfree = $row[0];
-
-    $sql = "select format(used/total*100,1), usable from cluster_space order by timestamp desc limit 1;";
-
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $qry->fetchInto($row);
-    $allocated = $row[0];
-    $usable    = $row[1];
-
-    #echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>";
-    echo "<script type=\"text/javascript\" src=\"loader.js\"></script>";
-    echo "<br><div class=\"chartWithOverlay\" style=\"position: relative; width: 640; height:400\">"; 
-    echo "  <div id=\"space_div\" style=\"width:640; \"></div>";
-    echo "  <div class=\"overlay\" style=\"position: absolute; width: 500px; bottom: 60px; left: 85px;\">";
-    echo "    <div style=\"font-size: 16px; color:#0000ff; \"><b>ipp*_bck: $bckusable TB (usable)</b></div>";
-    echo "    <div style=\"font-size: 16px; color:#33a532; \"><b>ipp*_bck: $bckfree   TB (free)</b></div>";
-    echo "  </div>";
-    echo "</div>";
-
-    echo "<script type=\"text/javascript\">";
-    echo "google.charts.load('current', {packages: ['corechart']});";
-    echo "google.charts.setOnLoadCallback(drawChart);";
-    echo "function drawChart() {";
-    echo "  var data = new google.visualization.DataTable();";
-    echo "        data.addColumn('datetime', 'Day');";
-    echo "        data.addColumn('number', 'Usable (ipp:$ippusable, ippb:$ippbusable) TB');";
-    echo "        data.addColumn('number', 'Free (ipp:$ippfree, ippb:$ippbfree) TB');";
-    echo "   data.addRows([";
-    $interval = 10;
-    $sql = "select sub.dated, sub.usable, sub.available from (select date_format(timestamp, \"%Y.%m.%d.%H.%i\") as dated, usable, available from cluster_space where timestamp > curdate() - INTERVAL $interval DAY) as sub group by dated;";
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    while ($qry->fetchInto($row)) {
-        $tsp = explode('.', $row[0]);
-	$mon = $tsp[1]-1;
-        $timestamp = $row[0];
-        $usable    = $row[1];
-        $available = $row[2];
-#	new Date(Year, Month, Day, Hours, Minutes, Seconds, Milliseconds)
-       echo "[new Date($tsp[0], $mon, $tsp[2], $tsp[3], $tsp[4]), $usable, $available],";
-    }
-   echo "  ]);";
-   echo "   var options = {";
-   echo "      chart: {";  
-   echo "               position: 'center', " ;
-   echo "              },";
-   echo "     title: 'Total Available Cluster Space in the Past $interval Days ($allocated% of total allocated)',";
-   echo "     titleTextStyle: {color: 'black', fontSize: 15},";
-   echo "     width: '100%',";
-   echo "     height: 400,";
-   echo "     legend: { position: 'top',";
-   echo "               alignment: 'center',";
-   echo "              },";
-   echo "     lineWidth: 2, ";
-   echo "     series: {";
-   echo "         0: { color: '#0000ff', lineWidth: 3 },      ";
-   echo "         1: { color: '#33a532', lineWidth: 3 },      ";
-   echo "              },";
-   echo "     hAxis: {  ";
-   echo "             title: 'Date/Time (HST)',";
-   echo "             format: 'MMM dd',";
-   echo "            gridlines: {count: -1},"; 
-   echo "     },";
-   echo "     vAxis: {  ";
-   echo "                  title: 'Available (TB)',";
-   echo "     viewWindow: {   min: 500    },  ";
-   echo "     },";
-   echo "     chartArea: {left:80, top:50, right:20, bottom:50},";
-   echo "     fontSize: 12,";
-   echo "   };";
-   echo "   var chart = new google.visualization.LineChart(document.getElementById('space_div'));";
-   echo "   chart.draw(data, options);";
-   echo " }";
-   echo "</script>";
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $finish = $time;
-    $total_time = round(($finish - $start), 3);
-    $start= $finish;
-    echo "<center>loading in $total_time seconds<br></center>";
-}
-
-###########################################################################
-#
-# Gets data quality status
-#
-###########################################################################
-function getDataQuality($db) {
-    // timer start
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $start = $time;
-
-    $date = gmdate("Y-m-d");
-    $sql = "SELECT MAX(exp_id) FROM rawExp WHERE dateobs BETWEEN DATE_SUB('$date', INTERVAL 20 DAY) AND DATE_SUB('$date', INTERVAL 10 DAY)";
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-
-    if (dberror($qry)) {
-        echo "*** WARNING: No data in the period 10-20 days before requested date (query will be slower) **\n";
-        $refExpID = 0;
-    } else {
-        $qry->fetchInto($refExpID);
-        $refExpID = isset($refExpID[0]) ? $refExpID[0] : 0;
-    }
-    // Print the result of the query
-    if ($debug) {echo "The result of MAX(exp_id) is: $refExpID<br>";}
-    {echo "The result of MAX(exp_id) is: $refExpID<br>";}
-
-    #Query for the needed data
-    $DQarray = array();
-
-    $sql = "SELECT subtime(dateobs,'10:00:00') as time,iq_fwhm_major,iq_fwhm_minor,zpt_obs,zpt_stdev,rawExp.filter,rawExp.exp_time,rawExp.comment FROM rawExp JOIN chipRun using (exp_id) JOIN camRun using (chip_id) JOIN camProcessedExp using (cam_id)";
-#    $sql.= " WHERE (rawExp.dateobs > utc_date() - INTERVAL 14 hour) and (rawExp.dateobs < utc_date() + INTERVAL 10 hour)";
-    $sql.= " WHERE (rawExp.dateobs > utc_date())";
-#    $sql.= " WHERE (rawExp.dateobs > '2024-10-24')";
-#    $sql.= " AND (rawExp.dateobs < '2024-10-25')";
-    $sql.= " AND exp_id > $refExpID[0]";
-    $sql.= " AND exp_type = 'OBJECT' AND camRun.state like 'full' ORDER BY dateobs asc;";
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-#    echo "<b>error with $sql </b><br>\n";
-    while ($qry->fetchInto($row)) {
-        $DQarray[] = $row;
-    }
-
-    #also find the median zeropoint
-#    foreach($DQarray as $row) {
-#        $zpt[]    = $row[3];
-#    }
-
-#    sort($zpt);
-#    $count = sizeof($zpt);   // cache the count
-#    $index = floor($count/2);  // cache the index
-#    $zpt_med = $zpt[$index];
-
-    #echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>";
-    echo "<script type=\"text/javascript\" src=\"loader.js\"></script>";
-    echo "<br><div class=\"chartWithOverlay\" style=\"position: relative; width: 640; height:400\">"; 
-    echo "  <div id=\"dq_div\" style=\"width:640; \"></div>";
-    echo "  <div class=\"overlay\" style=\"position: absolute; width: 400px; bottom: 60px; left: 85px;\">";
-    echo "  </div>";
-    echo "</div>";
-
-    echo "<script type=\"text/javascript\">";
-    echo "google.charts.load('current', {packages: ['corechart']});";
-    echo "google.charts.setOnLoadCallback(drawChart);";
-    echo "function drawChart() {";
-    echo "  var data = new google.visualization.DataTable();";
-    echo "        data.addColumn('datetime', 'Time');";
-    echo "        data.addColumn('number', 'FWHM_maj');";
-    echo "        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});";
-    echo "        data.addColumn('number', 'FWHM_min');";
-    echo "        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});";
-    echo "        data.addColumn('number', 'seeing=1');";
-    echo "        data.addColumn('number', 'seeing=1.5');";
-    echo "   data.addRows([";
-
-    foreach($DQarray as $row) {
-        $tsp = explode(' ', $row[0]);
-        $dat = explode('-', $tsp[0]);
-        $tm = explode(':', $tsp[1]);
-        $timestamp = $row[0];
-        $fwhm_maj    = $row[1];
-        $fwhm_min = $row[2];
-
-        $tt_string1 = sprintf('time: %s\n FWHM_major: %s\n filter: %s\n exp_time: %s\n comment: %s', $timestamp, $fwhm_maj,$row[5],$row[6],$row[7]);
-        $tt_string2 = sprintf('time: %s\n FWHM_minor: %s\n filter: %s\n exp_time: %s\n comment: %s', $timestamp, $fwhm_min,$row[5],$row[6],$row[7]);
-#	new Date(Year, Month, Day, Hours, Minutes, Seconds, Milliseconds)
-       echo "[new Date($dat[0], $dat[1]-1, $dat[2], $tm[0], $tm[1]), $fwhm_maj,'$tt_string1',$fwhm_min,'$tt_string2',3.85,5.76],";
-    }
-   echo "  ]);";
-   echo "   var options = {";
-   echo "      chart: {";  
-   echo "               position: 'center', " ;
-   echo "              },";
-   echo "     title: 'FWHM overview during the last night',";
-   echo "     titleTextStyle: {color: 'black', fontSize: 15},";
-   echo "     width: '100%',";
-   echo "     height: 400,";
-   echo "     legend: { position: 'top',";
-   echo "               alignment: 'center',";
-   echo "              },";
-   echo "     lineWidth: 2, ";
-   echo "     series: {";
-   echo "         0: { color: '#0000ff', lineWidth: 3 },      ";
-   echo "         1: { color: '#33a532', lineWidth: 3 },      ";
-   echo "         2: { color: 'red', lineWidth: 2 },      ";
-   echo "         3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4] },      ";
-   echo "              },";
-   echo "     hAxis: {  ";
-   echo "             title: 'Date/Time (HST)',";
-   echo "             format: 'HH:mm',";
-   echo "            gridlines: {count: -1},"; 
-   echo "     },";
-   echo "     vAxis: {  ";
-   echo "                  title: 'FWHM (pixels)',";
-   echo "     },";
-   echo "     chartArea: {left:80, top:50, right:20, bottom:50},";
-   echo "     fontSize: 12,";
-   echo "   };";
-   echo "   var chart = new google.visualization.LineChart(document.getElementById('dq_div'));";
-   echo "   chart.draw(data, options);";
-   echo " }";
-   echo "</script>";
-
-#   ---------------------------------------
-#   Also the zeropoint
-
-    #echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>";
-    echo "<script type=\"text/javascript\" src=\"loader.js\"></script>";
-    echo "<br><div class=\"chartWithOverlay\" style=\"position: relative; width: 640; height:400\">"; 
-    echo "  <div id=\"dqzp_div\" style=\"width:640; \"></div>";
-    echo "  <div class=\"overlay\" style=\"position: absolute; width: 400px; bottom: 60px; left: 85px;\">";
-    echo "  </div>";
-    echo "</div>";
-
-    echo "<script type=\"text/javascript\">";
-    echo "google.charts.load('current', {packages: ['corechart']});";
-    echo "google.charts.setOnLoadCallback(drawChart);";
-    echo "function drawChart() {";
-    echo "  var data = new google.visualization.DataTable();";
-    echo "        data.addColumn('datetime', 'Time');";
-    echo "        data.addColumn('number', 'zeropoint');";
-    echo "        data.addColumn({'type': 'string', 'role': 'style'});";
-    echo "        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});";
-    echo "        data.addColumn({id:'i0', type:'number', role:'interval'});";
-    echo "        data.addColumn({id:'i0','type': 'string', 'role': 'style'});";
-    echo "        data.addColumn({id:'i0', type:'number', role:'interval'});";
-    echo "        data.addColumn({id:'i0','type': 'string', 'role': 'style'});";
-    echo "   data.addRows([";
-    foreach($DQarray as $row) {
-        $tsp = explode(' ', $row[0]);
-        $dat = explode('-', $tsp[0]);
-        $tm = explode(':', $tsp[1]);
-        $timestamp = $row[0];
-        $zp    = $row[3];
-        $zp_stdev = $row[4];
-        $zp_low = $zp-$zp_stdev;
-        $zp_high = $zp+$zp_stdev;
-        $col_string = '#0000ff';
-        $zpt_off = 0.;
-
-        $tt_string1 = sprintf('time: %s\n zeropoint: %s\n filter: %s\n exp_time: %s\n comment: %s', $timestamp, $zp,$row[5],$row[6],$row[7]);
-#	new Date(Year, Month, Day, Hours, Minutes, Seconds, Milliseconds)
-       echo "[new Date($dat[0], $dat[1]-1, $dat[2], $tm[0], $tm[1]), $zp,'$col_string','$tt_string1', $zp_low,'$col_string', $zp_high,'$col_string'],";
-    }
-
-   echo "  ]);";
-   echo "   var options = {";
-   echo "      chart: {";  
-   echo "               position: 'center', " ;
-   echo "              },";
-   echo "     title: 'Zeropoint overview during the last night',";
-   echo "     titleTextStyle: {color: 'black', fontSize: 15},";
-   echo "     width: '100%',";
-   echo "     height: 400,";
-   echo "     legend: { position: 'top',";
-   echo "               alignment: 'center',";
-   echo "              },";
-   echo "     series: {";
-   echo "         0: { color: '#0000ff'},      ";
-   echo "              },";
-   echo "     intervals: { color: 'grey','style':'points', pointSize: 1 },  ";
-   echo "     hAxis: {  ";
-   echo "             title: 'Date/Time (HST)',";
-   echo "             format: 'HH:mm',";
-   echo "            gridlines: {count: -1},"; 
-   echo "     },";
-   echo "     vAxis: {  ";
-   echo "                  title: 'Zeropoint (mag)',";
-   echo "     },";
-   echo "     chartArea: {left:80, top:50, right:20, bottom:50},";
-   echo "     fontSize: 12,";
-   echo "   };";
-   echo "   var chart = new google.visualization.ScatterChart(document.getElementById('dqzp_div'));";
-   echo "   chart.draw(data, options);";
-   echo " }";
-   echo "</script>";
-
-#   ---------------------------------------
-#   And a fixed zoom in on the zeropoint to see details
-
-    #echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>";
-    echo "<script type=\"text/javascript\" src=\"loader.js\"></script>";
-    echo "<br><div class=\"chartWithOverlay\" style=\"position: relative; width: 640; height:400\">"; 
-    echo "  <div id=\"dqzp_zoom_div\" style=\"width:640; \"></div>";
-    echo "  <div class=\"overlay\" style=\"position: absolute; width: 400px; bottom: 60px; left: 85px;\">";
-    echo "  </div>";
-    echo "</div>";
-
-    echo "<script type=\"text/javascript\">";
-    echo "google.charts.load('current', {packages: ['corechart']});";
-    echo "google.charts.setOnLoadCallback(drawChart);";
-    echo "function drawChart() {";
-    echo "  var data = new google.visualization.DataTable();";
-    echo "        data.addColumn('datetime', 'Time');";
-    echo "        data.addColumn('number', 'zeropoint');";
-    echo "        data.addColumn({'type': 'string', 'role': 'style'});";
-    echo "        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});";
-    echo "        data.addColumn({id:'i0', type:'number', role:'interval'});";
-    echo "        data.addColumn({id:'i1', type:'number', role:'interval'});";
-    echo "   data.addRows([";
-    foreach($DQarray as $row) {
-        $tsp = explode(' ', $row[0]);
-        $dat = explode('-', $tsp[0]);
-        $tm = explode(':', $tsp[1]);
-        $timestamp = $row[0];
-        $zp    = $row[3];
-        $zp_stdev = $row[4];
-        $zp_low = $zp_stdev;
-        $zp_high = $zp_stdev;
-        $col_string = '#0000ff';
-        $zpt_off = 0.;
-
-        #if(substr($row[5],0,1) == 'g') {$col_string = 'blue'; $zpt_off = $zp - 24.563;}
-        #if(substr($row[5],0,1) == 'r') {$col_string = 'red'; $zpt_off = $zp - 24.740;}
-        #if(substr($row[5],0,1) == 'i') {$col_string = '#6a3d9a'; $zpt_off = $zp - 24.611;}
-        #if(substr($row[5],0,1) == 'z') {$col_string = 'green'; $zpt_off = $zp - 24.250;}
-        #if(substr($row[5],0,1) == 'y') {$col_string = 'cyan'; $zpt_off = $zp - 23.320;}
-        #if(substr($row[5],0,1) == 'w') {$col_string = 'grey'; $zpt_off = $zp - 26.300;}
-
-        #define new zeropoints
-        if(substr($row[5],0,1) == 'g') {$col_string = '#00f000'; $zpt_off = $zp - 25.013;}
-        if(substr($row[5],0,1) == 'r') {$col_string = '#f00000'; $zpt_off = $zp - 24.840;}
-        if(substr($row[5],0,1) == 'i') {$col_string = '#f05000'; $zpt_off = $zp - 24.711;}
-        if(substr($row[5],0,1) == 'z') {$col_string = '#0050a0'; $zpt_off = $zp - 24.300;}
-        if(substr($row[5],0,1) == 'y') {$col_string = '#ffe000'; $zpt_off = $zp - 23.570;}
-        if(substr($row[5],0,1) == 'w') {$col_string = '#808080'; $zpt_off = $zp - 26.060;}
-
-        $tt_string1 = sprintf('time: %s\n zeropoint: %s\n filter: %s\n exp_time: %s\n comment: %s', $timestamp, $zp,$row[5],$row[6],$row[7]);
-#	new Date(Year, Month, Day, Hours, Minutes, Seconds, Milliseconds)
-       echo "[new Date($dat[0], $dat[1]-1, $dat[2], $tm[0], $tm[1]), $zpt_off,'$col_string','$tt_string1', $zp_low, $zp_high],";
-    }
-
-   echo "  ]);";
-   echo "   var options = {";
-   echo "      chart: {";  
-   echo "               position: 'center', " ;
-   echo "              },";
-   echo "     title: 'Zeropoint zoom-in during the last night',";
-   echo "     titleTextStyle: {color: 'black', fontSize: 15},";
-   echo "     width: '100%',";
-   echo "     height: 400,";
-   echo "     legend: { position: 'top',";
-   echo "               alignment: 'center',";
-   echo "              },";
-   echo "     pointSize: 3, ";
-   echo "     series: {";
-   echo "         0: { color: '#0000ff'},      ";
-   echo "              },";
-   echo "     intervals: { color: 'grey','style':'points', pointSize: 1 },  ";
-   echo "     hAxis: {  ";
-   echo "             title: 'Date/Time (HST)',";
-   echo "             format: 'HH:mm',";
-   echo "            gridlines: {count: -1},"; 
-   echo "     },";
-   echo "     vAxis: {  ";
-   echo "                  title: 'Zeropoint offset (mag)',";
-   echo "     viewWindow: {   min: -0.25, max: 0.25    },  ";
-   echo "     },";
-   echo "     chartArea: {left:80, top:50, right:20, bottom:50},";
-   echo "     fontSize: 12,";
-   echo "   };";
-   echo "   var chart = new google.visualization.ScatterChart(document.getElementById('dqzp_zoom_div'));";
-   echo "   chart.draw(data, options);";
-   echo " }";
-   echo "</script>";
-
-
-
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $finish = $time;
-    $total_time = round(($finish - $start), 3);
-    $start= $finish;
-    echo "<center>loading plot in $total_time seconds<br></center>";
-}
-
 
 
@@ -3269,370 +2840,4 @@
 }
 
-
-###########################################################################
-#
-# Get data quality
-#
-###########################################################################
-function getDataQuality2($db) {
-    // Start timer
-    $start = microtime(true);
-
-    $mjdDay  = getMJD();
-    $expname = "o".$mjdDay."%";
-
-    $sql = "SELECT SUBTIME(dateobs, '10:00:00') AS time, 
-                   round(( 367*YEAR(dateobs)- FLOOR((7 * (YEAR(dateobs) + FLOOR((MONTH(dateobs) + 9) / 12))) / 4) + FLOOR(275 * MONTH(dateobs) / 9) 
-                   + DAY(dateobs) + 1721013.5 + (HOUR(dateobs) / 24.0) + (MINUTE(dateobs) / 1440.0) + (SECOND(dateobs) / 86400.0) - 2400000.5), 5) AS MJD, 
-                   filter, comment, ra*180/pi() as RA, decl*180/pi() as DECL, exp_name, alt, az, fwhm_major, fwhm_minor, zpt_obs, zpt_stdev, rawExp.exp_id 
-            FROM rawExp JOIN chipRun using (exp_id) JOIN camRun using (chip_id) JOIN camProcessedExp using (cam_id) 
-            WHERE rawExp.exp_name like '$expname'
-               AND exp_type = 'OBJECT' 
-               AND camRun.state like 'full' 
-               AND camProcessedExp.fault = 0 
-               AND camProcessedExp.zpt_obs != 0; ";
-
-    $DQarray = array();
-    $qry = $db->query($sql);
-    if (dberror($qry)) {
-        echo "<b>Error with $sql</b><br>\n";
-        return;
-    }
-    
-    while ($qry->fetchInto($row)) {
-        $DQarray[] = $row;
-    }
-    
-    // Ensure $DQarray is not empty
-    if (empty($DQarray)) {
-        echo "<center><b> No data for MJD: $mjdDay </b></center>";
-        return; // Stop execution if no data to encode
-    }
-    
-    // Convert data array to JSON for easier handling in JavaScript
-    $dataJson = json_encode($DQarray);
-    
-    if ($dataJson === false) {
-        echo "<b>Error encoding JSON:</b> " . json_last_error_msg(); // Display any JSON encoding errors
-        return; // Stop execution if JSON encoding fails
-    }
-    
-    echo "<script>console.log(" . json_encode($dataJson) . ");</script>"; // Log JSON output to console for debugging
-
-echo <<<HTML
-<script type="text/javascript" src="loader.js"></script>
-
-<script type="text/javascript">
-    google.charts.load('current', {packages: ['corechart']});
-
-    // Use the encoded JSON directly in JavaScript
-    const dataArray = JSON.parse('{$dataJson}'); // Directly parse the PHP JSON string
-
-    // draw FWHM vs MJD
-    function drawFWHMChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'MJD');
-            data.addColumn('number', 'FWHM_major');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'FWHM_minor');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'seeing ~ 1"');
-            data.addColumn('number', 'seeing ~ 3"');
-
-        const rows = dataArray.map(row => {
-            const [timestamp, mjd, filter, comment, ra, dec, exp_name, alt, az, iq_fwhm_maj, iq_fwhm_min, zpt_obs, zpt_stdev, exp_id] = row;
-
-            return [
-                parseFloat(mjd),      // Position Angle
-                parseFloat(iq_fwhm_maj),         // RA_offset (AST_R0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,
-                parseFloat(iq_fwhm_min),        // Dec_offset (AST_D0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,
-                4,
-                12
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'FWHM (MJD)',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#0000ff', pointSize: 3, lineWidth: 1 },
-                1: { color: '#33a532', pointSize: 3, lineWidth: 1 },
-                2: { color: 'red', lineWidth: 2, pointSize: 0 },
-                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'FWHM (pixels)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            chartArea: {left: '8%', top: 60, right: '5%', bottom: 50}, // Adjust left and right
-            fontSize: 12,
-        };
-
-        const chart = new google.visualization.LineChart(document.getElementById('dq_div_fwhm'));
-        chart.draw(data, options);
-    }
-
-    // draw zoomed zerpoint vs mjd
-    function drawZOOMZPTChart() {
-        var data = new google.visualization.DataTable();
-        data.addColumn('number', 'MJD');
-
-        // Add columns for each filter type to handle separate colors
-        data.addColumn('number', 'g (24.563, 25.013)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'r (24.750, 24.840)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'i (24.611, 24.711)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'z (24.240, 24.300)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'y (23.320, 23.570)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'w (26.000, 26.300)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-
-        const rows = dataArray.map(row => {
-            const [timestamp, mjd, filter, comment, ra, dec, exp_name, alt, az, iq_fwhm_maj, iq_fwhm_min, zpt_obs, zpt_stdev, exp_id] = row;
-
-            // Initialize all filter values as null
-            let zp_g = null, zp_r = null, zp_i = null, zp_z = null, zp_y = null, zp_w = null;
-
-            // Set the zeropoint offset based on filter and assign it to the correct series
-            switch (filter[0]) {
-                case 'g':
-                    zp_g = zpt_obs - 24.563;
-                    break;
-                case 'r':
-                    zp_r = zpt_obs - 24.750;
-                    break;
-                case 'i':
-                    zp_i = zpt_obs - 24.611;
-                    break;
-                case 'z':
-                    zp_z = zpt_obs - 24.240;
-                    break;
-                case 'y':
-                   zp_y = zpt_obs - 23.320;
-                    break;
-                case 'w':
-                    zp_w = zpt_obs - 26.000;
-                    break;
-            }
-
-            return [
-                parseFloat(mjd), // MJD value
-                zp_g, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_g - zpt_stdev), parseFloat(zp_g - zpt_stdev*-1),
-                zp_r, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_r - zpt_stdev), parseFloat(zp_r - zpt_stdev*-1),
-                zp_i, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_i - zpt_stdev), parseFloat(zp_i - zpt_stdev*-1),
-                zp_z, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_z - zpt_stdev), parseFloat(zp_z - zpt_stdev*-1),
-                zp_y, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_y - zpt_stdev), parseFloat(zp_y - zpt_stdev*-1),
-                zp_w, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_w - zpt_stdev), parseFloat(zp_w - zpt_stdev*-1)
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'Zeropoint zoom-in (MJD)',
-            titleTextStyle: { color: 'black', fontSize: 15 },
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'start', maxLines: 3 },
-            series: {
-                0: { color: '#00f000', pointSize: 10, pointShape: 'circle',   linewidth: 1},  // 'g' filter
-                1: { color: '#f00000', pointSize: 10, pointShape: 'triangle', linewidth: 1},  // 'r' filter
-                2: { color: '#f05000', pointSize: 10, pointShape: 'square',   linewidth: 1},  // 'i' filter
-                3: { color: '#0050a0', pointSize: 10, pointShape: 'diamond',  linewidth: 1},  // 'z' filter
-                4: { color: '#ffe000', pointSize: 10, pointShape: 'star',     linewidth: 1},  // 'y' filter
-                5: { color: '#808080', pointSize: 10, pointShape: 'polygon',  linewidth: 1},  // 'w' filter
-            },
-            intervals: { color: 'red', lineWidth: 1, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: { count: -1 }
-            },
-            vAxis: {
-                title: 'Zeropoint (mag)',
-                viewWindow: {   min: -1.00, max: .5    },
-            },
-            tooltip: { isHtml: true },
-            chartArea: { left: '9%', top: 60, right: '5%', bottom: 50 },
-            fontSize: 12,
-        };
-
-        const chart = new google.visualization.LineChart(document.getElementById('dq_div_zoomzpt'));
-        chart.draw(data, options);
-    }
-
-    // draw zeropoint vs mjd
-    function drawZPTChart() {
-        var data = new google.visualization.DataTable();
-        data.addColumn('number', 'MJD');
-
-        // Add columns for each filter type to handle separate colors
-        data.addColumn('number', 'g' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'r' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'i' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'z' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'y' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'w' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-
-        // Count occurrences of each filter
-        const filterCounts = { g: 0, r: 0, i: 0, z: 0, y: 0, w: 0 };
-       const rows = dataArray.map(row => {
-            const [timestamp, mjd, filter, comment, ra, dec, exp_name, alt, az, iq_fwhm_maj, iq_fwhm_min, zpt_obs, zpt_stdev, exp_id] = row;
-
-            // Initialize all filter values as null
-            let zp_g = null, zp_r = null, zp_i = null, zp_z = null, zp_y = null, zp_w = null;
-
-            // Increment the count for the filter
-            if (filterCounts.hasOwnProperty(filter[0])) {
-                filterCounts[filter[0]] += 1;
-            }
-
-            // Set the zeropoint offset based on filter and assign it to the correct series
-            switch (filter[0]) {
-                case 'g':
-                    zp_g = zpt_obs - 0.0;
-                    break;
-                case 'r':
-                    zp_r = zpt_obs - 0.0;
-                    break;
-                case 'i':
-                    zp_i = zpt_obs - 0.0;
-                    break;
-                case 'z':
-                    zp_z = zpt_obs - 0.0;
-                    break;
-                case 'y':
-                    zp_y = zpt_obs - 0.0;
-                    break;
-                case 'w':
-                    zp_w = zpt_obs - 0.0;
-                    break;
-            }
-
-            return [
-                parseFloat(mjd), // MJD value
-                zp_g, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_g - zpt_stdev), parseFloat(zp_g - zpt_stdev*-1),
-                zp_r, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_r - zpt_stdev), parseFloat(zp_r - zpt_stdev*-1),
-                zp_i, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_i - zpt_stdev), parseFloat(zp_i - zpt_stdev*-1),
-                zp_z, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_z - zpt_stdev), parseFloat(zp_z - zpt_stdev*-1),
-                zp_y, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_y - zpt_stdev), parseFloat(zp_y - zpt_stdev*-1),
-                zp_w, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment,  parseFloat(zp_w - zpt_stdev), parseFloat(zp_w - zpt_stdev*-1),
-
-            ];
-        });
-      data.addRows(rows);
-
-        // Include counts in the title using string concatenation
-        const countsTitle = 'Zeropoint (MJD) g: ' + filterCounts.g + ', r: ' + filterCounts.r + ', i: ' + filterCounts.i + ', z: ' + filterCounts.z + ', y: ' + filterCounts.y + ', w: ' + filterCounts.w;
-
-
-        console.log(countsTitle);
-
-        const options = {
-            title: countsTitle,
-            titleTextStyle: { color: 'black', fontSize: 15 },
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#00f000', pointSize: 10, pointShape: 'circle',   linewidth: 1},  // 'g' filter
-                1: { color: '#f00000', pointSize: 10, pointShape: 'triangle', linewidth: 1},  // 'r' filter
-                2: { color: '#f05000', pointSize: 10, pointShape: 'square',   linewidth: 1},  // 'i' filter
-                3: { color: '#0050a0', pointSize: 10, pointShape: 'diamond',  linewidth: 1},  // 'z' filter
-                4: { color: '#ffe000', pointSize: 10, pointShape: 'star',     linewidth: 1},  // 'y' filter
-                5: { color: '#808080', pointSize: 10, pointShape: 'polygon',  linewidth: 1},  // 'w' filter
-            },
-            intervals: { color: 'red', lineWidth: 1, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: { count: -1 }
-            },
-            vAxis: {
-                title: 'Zeropoint (mag)'
-            },
-            tooltip: { isHtml: true },
-            chartArea: { left: '9%', top: 60, right: '5%', bottom: 50 },
-            fontSize: 12,
-        };
-
-        const chart = new google.visualization.LineChart(document.getElementById('dq_div_zpt'));
-        chart.draw(data, options);
-    }
-
-    google.charts.setOnLoadCallback(drawFWHMChart);
-    google.charts.setOnLoadCallback(drawZPTChart);
-    google.charts.setOnLoadCallback(drawZOOMZPTChart);
-
-</script>
-
-<div class="chartWithOverlay" style="position: relative; width: 640px; height:500px;">
-    <div id="dq_div_fwhm" style="width:640px;"></div>
-    <div class="overlay" style="position: absolute; width: 400px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 640px; height:500px;">
-    <div id="dq_div_zpt" style="width:640px;"></div>
-    <div class="overlay" style="position: absolute; width: 400px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 640px; height:500px;">
-    <div id="dq_div_zoomzpt" style="width:640px;"></div>
-    <div class="overlay" style="position: absolute; width: 400px; bottom: 60px; left: 85px;"></div>
-</div>
-
-HTML;
-    // End timer
-    $total_time = round(microtime(true) - $start, 3);
-    echo "<center>loading plot in $total_time seconds<br></center>";
-
-}
-
-
-
 function getprocrate ($proc1, $proc0, $trange) {
   if($proc1 > $proc0) {
@@ -3646,4 +2851,3 @@
 
 
-
 ?>
Index: /trunk/ippMonitor/raw/dataquality.php
===================================================================
--- /trunk/ippMonitor/raw/dataquality.php	(revision 42971)
+++ /trunk/ippMonitor/raw/dataquality.php	(revision 42972)
@@ -15,5 +15,5 @@
 else {$myMenu = "ipp.czar.dat";}
 
-menu($myMenu, 'Czartool on '.$lastUpdateTime, 'ipp.css', $ID['link'], $ID['proj']);
+menu($myMenu, 'Czartool on', ' ipp.css', $ID['link'], $ID['proj']);
 
 $pass = $ID['pass'];
@@ -22,5 +22,7 @@
 
 // Fetch data based on range selection
-$range = isset($_GET['range']) ? $_GET['range'] : 'yday';
+$range = isset($_GET['range']) ? $_GET['range'] : 'week';
+
+require_once 'functions.php';
 
 echo "<table border=\"0\">";
@@ -28,495 +30,49 @@
 echo "  <td style=width:1240>";
 echo "    <h1 align=\"middle\">Data Quality Report</h1>";
-echo "  <center>";
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=yday">Last night (default)</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=day">Today (MJD '.getMJD().')</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=week">Past 7 days</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=month">Past 30 days</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=year">Past 365 days</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=all">All </a>   ';
+
+#
+# project selector
+echo '<center>';
+# Project selector with highlight
+$projects = ["gpc1" => "GPC1", "gpc2" => "GPC2"];
+foreach ($projects as $key => $label) {
+    if ($proj == $key) {
+        echo "<strong style='color:blue;'>$label</strong> | ";
+    } else {
+        echo '<a href="?pass=' . urlencode($pass) . '&proj=' . $key . '&range=' . urlencode($range) . '">' . $label . '</a> | ';
+    }
+}
+echo "<br>";
+
+# range links with highlighting
+$ranges = [
+    "today"     => "Today",
+    "yesterday" => "Yesterday",
+    "week"      => "Week",
+    "month"     => "Month",
+    "year"      => "Year",
+    "all"       => "All"
+];
+
+foreach ($ranges as $key => $label) {
+    if ($range == $key) {
+        echo "<strong style='color:blue;'>$label</strong> | ";
+    } else {
+        echo '<a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=' . $key . '">' . $label . '</a> | ';
+    }
+}
+
 echo "  </td>";
 echo "</tr>";
 echo "</table>";
-echo getDataQuality($projectdb, $range);
+
+echo "    <div style=\"margin-bottom:10px; font-size:14px; color:#555;\">";
+echo "      Current MJD: " . getMJD();
+echo "    </div>";
+
+echo getDataQuality($projectdb, $range, $proj);
 
 menu_end();
 
-/////////////////////////////////////////////////////////////////////////////
-//                                                                         //
-//                               Functions                                 //
-//                                                                         //
-/////////////////////////////////////////////////////////////////////////////
-
-###########################################################################
-#
-# Get data quality
-#
-###########################################################################
-function getDataQuality($db, $range = 'yday') {
-    // Start timer
-    $start = microtime(true);
-
-    $mjdDay  = getMJD();
-    $mjdDay_yearback = $mjdDay - 366;
-    $mjdDay_monthback = $mjdDay - 32;
-    $min_exp_id = 0;
-
-    // Determine the reference exp_name based on the selected range
-    switch ($range) {
-        case 'year':
-            $expname = "o" . $mjdDay_yearback . "%";
-            break;
-        case 'month':
-            $expname = "o" . $mjdDay_monthback . "%";
-            break;
-        case 'week':
-            $expname = "o" . ($mjdDay - 7) . "%";
-            break;
-        case 'day':
-            $expname = "o" . $mjdDay . "%";
-            break;
-        default:
-            $expname = "o" . ($mjdDay - 1) . "%"; // Default: last night
-            break;
-    }
-
-    // Query to get the minimum exp_id
-    $expIdQuery = "SELECT MIN(exp_id) AS min_exp_id FROM rawExp WHERE rawExp.exp_name LIKE '$expname'";
-    $result = $db->query($expIdQuery);
-
-    if (DB::isError($result)) {
-        die("Database error: " . $result->getMessage());
-    }
-
-    $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
-
-    if (!empty($row['min_exp_id'])) {
-        $min_exp_id = $row['min_exp_id'];
-    //    echo "Minimum exp_id: " . $min_exp_id;
-    } else {
-        echo "No results found.";
-    }
-
-    //echo "<br><br>";
-
-    // Determine the date constraint based on the range
-    switch ($range) {
-        case 'year':
-            $dateConstraint = "dateobs >= NOW() - INTERVAL 365 DAY AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'month':
-            $dateConstraint = "dateobs >= NOW() - INTERVAL 30 DAY AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'week':
-            $expnames = array();
-            for ($i = 0; $i < 7; $i++) {
-                $expnames[] = "rawExp.exp_name LIKE 'o" . ($mjdDay - $i) . "%'";
-            }
-            $dateConstraint = "(" . implode(" OR ", $expnames) . ") AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'yday':
-            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'day':
-            $dateConstraint = "rawExp.exp_name LIKE 'o$mjdDay%' AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'all':
-            $dateConstraint = "rawExp.exp_id >= 0";
-            break;
-        default: // Default to last night
-            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
-            break;
-    }
-
-    // Updated SQL query
-    $sql = "select * from (SELECT SUBTIME(dateobs, '10:00:00') AS time,
-                   round(( 367*YEAR(dateobs)- FLOOR((7 * (YEAR(dateobs) + FLOOR((MONTH(dateobs) + 9) / 12))) / 4) + FLOOR(275 * MONTH(dateobs) / 9)
-                   + DAY(dateobs) + 1721013.5 + (HOUR(dateobs) / 24.0) + (MINUTE(dateobs) / 1440.0) + (SECOND(dateobs) / 86400.0) - 2400000.5), 5) AS MJD,
-                   filter, comment, ra*180/pi() as RA, decl*180/pi() as DECL, exp_name, alt, az, fwhm_major, fwhm_minor, zpt_obs, zpt_stdev, rawExp.exp_id, camRun.label
-            FROM rawExp 
-            JOIN chipRun USING (exp_id) 
-            JOIN camRun USING (chip_id) 
-            JOIN camProcessedExp USING (cam_id)
-            WHERE $dateConstraint
-              AND exp_type = 'OBJECT'
-              AND camRun.state LIKE 'full'
-              AND camProcessedExp.fault = 0
-              AND camProcessedExp.zpt_obs != 0 ORDER BY RAND() limit 5000) as sub order by time;";
-
-    $DQarray = array();
-    $qry = $db->query($sql);
-    if (dberror($qry)) {
-        echo "<b>Error with $sql</b><br>\n";
-        return;
-    }
-    //    echo "$sql<br><br>\n";
-
-    while ($qry->fetchInto($row)) {
-        $DQarray[] = $row;
-    }
-
-    // Ensure $DQarray is not empty
-    if (empty($DQarray)) {
-        echo "<center><b> No data for MJD: $mjdDay </b></center>";
-        return; // Stop execution if no data to encode
-    }
-
-    // Convert data array to JSON for easier handling in JavaScript
-    $dataJson = json_encode($DQarray);
-
-    if ($dataJson === false) {
-        echo "<b>Error encoding JSON:</b> " . json_last_error_msg(); // Display any JSON encoding errors
-        return; // Stop execution if JSON encoding fails
-    }
-
-    echo "<script>console.log(" . json_encode($dataJson) . ");</script>"; // Log JSON output to console for debugging
-echo <<<HTML
-<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js" onerror="loadLocalGoogleCharts()"></script>
-
-<script type="text/javascript">
-    // Check if Google Charts library loaded successfully
-    window.addEventListener("load", function() {
-        if (typeof google === "undefined" || typeof google.charts === "undefined") {
-            loadLocalGoogleCharts();
-        }
-    });
-
-    // Function to load local version of Google Charts if CDN fails
-    function loadLocalGoogleCharts() {
-        console.warn("Google Charts CDN failed, loading local loader.js instead.");
-        var script = document.createElement("script");
-        script.src = "loader.js"; // Path to your local loader.js file
-        document.head.appendChild(script);
-    }
-</script>
-
-<script type="text/javascript">
-    google.charts.load('current', {packages: ['corechart']});
-
-    // Use the encoded JSON directly in JavaScript
-    const dataArray = JSON.parse('{$dataJson}'); // Directly parse the PHP JSON string
-
-    // draw FWHM vs MJD
-    function drawFWHMChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'MJD');
-            data.addColumn('number', 'FWHM_major');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'FWHM_minor');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'seeing ~ 1"');
-            data.addColumn('number', 'seeing ~ 3"');
-
-        const rows = dataArray.map(row => {
-            const [timestamp, mjd, filter, comment, ra, dec, exp_name, alt, az, iq_fwhm_maj, iq_fwhm_min, zpt_obs, zpt_stdev, exp_id, cam_label] = row;
-
-            return [
-                parseFloat(mjd),      // Position Angle
-                parseFloat(iq_fwhm_maj),         // RA_offset (AST_R0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                parseFloat(iq_fwhm_min),        // Dec_offset (AST_D0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                4,
-                12
-            ];
-        });
-
-        data.addRows(rows);
-
-        // Extract the first MJD value from dataArray
-        //const firstMJD = dataArray.length > 0 ? parseFloat(dataArray[0][1]) : "N/A"; 
-        const firstMJD = dataArray.length > 0 ? Math.floor(parseFloat(dataArray[0][1])) : "N/A";
-        console.log("First MJD Value:", firstMJD);
-
-        const options = {
-            title: 'FWHM (MJD: '+firstMJD+')',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#0000ff', pointSize: 2, lineWidth: 0. },
-                1: { color: '#33a532', pointSize: 2, lineWidth: 0. },
-                2: { color: 'red', lineWidth: 2, pointSize: 0 },
-                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'FWHM (pixels)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            chartArea: {left: '8%', top: 60, right: '5%', bottom: 50}, // Adjust left and right
-            fontSize: 12,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_fwhm'));
-        chart.draw(data, options);
-    }
-
-    // draw zoomed zerpoint vs mjd
-    function drawZOOMZPTChart() {
-        var data = new google.visualization.DataTable();
-        data.addColumn('number', 'MJD');
-
-        // Add columns for each filter type to handle separate colors
-        data.addColumn('number', 'g (24.563, 25.013)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'r (24.750, 24.840)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'i (24.611, 24.711)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'z (24.240, 24.300)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'y (23.320, 23.570)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'w (26.000, 26.300)');
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-
-        const rows = dataArray.map(row => {
-            const [timestamp, mjd, filter, comment, ra, dec, exp_name, alt, az, iq_fwhm_maj, iq_fwhm_min, zpt_obs, zpt_stdev, exp_id, cam_label] = row;
-
-            // Initialize all filter values as null
-            let zp_g = null, zp_r = null, zp_i = null, zp_z = null, zp_y = null, zp_w = null;
-
-            // Set the zeropoint offset based on filter and assign it to the correct series
-            switch (filter[0]) {
-                case 'g':
-                    zp_g = zpt_obs - 24.563;
-                    break;
-                case 'r':
-                    zp_r = zpt_obs - 24.750;
-                    break;
-                case 'i':
-                    zp_i = zpt_obs - 24.611;
-                    break;
-                case 'z':
-                    zp_z = zpt_obs - 24.240;
-                    break;
-                case 'y':
-                   zp_y = zpt_obs - 23.320;
-                    break;
-                case 'w':
-                    zp_w = zpt_obs - 26.000;
-                    break;
-            }
-
-            return [
-                parseFloat(mjd), // MJD value
-                zp_g, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_g - zpt_stdev), parseFloat(zp_g - zpt_stdev*-1),
-                zp_r, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_r - zpt_stdev), parseFloat(zp_r - zpt_stdev*-1),
-                zp_i, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_i - zpt_stdev), parseFloat(zp_i - zpt_stdev*-1),
-                zp_z, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_z - zpt_stdev), parseFloat(zp_z - zpt_stdev*-1),
-                zp_y, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_y - zpt_stdev), parseFloat(zp_y - zpt_stdev*-1),
-                zp_w, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_w - zpt_stdev), parseFloat(zp_w - zpt_stdev*-1)
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'Zeropoint zoom-in (MJD)',
-            titleTextStyle: { color: 'black', fontSize: 15 },
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'start', maxLines: 3 },
-            series: {
-                0: { color: '#00f000', pointSize: 5, pointShape: 'circle',   linewidth: .0},  // 'g' filter
-                1: { color: '#f00000', pointSize: 5, pointShape: 'triangle', linewidth: .0},  // 'r' filter
-                2: { color: '#f05000', pointSize: 5, pointShape: 'square',   linewidth: .0},  // 'i' filter
-                3: { color: '#0050a0', pointSize: 5, pointShape: 'diamond',  linewidth: .0},  // 'z' filter
-                4: { color: '#ffe000', pointSize: 5, pointShape: 'star',     linewidth: .0},  // 'y' filter
-                5: { color: '#808080', pointSize: 5, pointShape: 'polygon',  linewidth: .0},  // 'w' filter
-            },
-            intervals: { color: 'red', lineWidth: .0, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: { count: -1 }
-            },
-            vAxis: {
-                title: 'Zeropoint (mag)',
-                viewWindow: {   min: -1.00, max: .5    },
-            },
-            tooltip: { isHtml: true },
-            chartArea: { left: '9%', top: 60, right: '5%', bottom: 50 },
-            fontSize: 12,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_zoomzpt'));
-        chart.draw(data, options);
-    }
-
-    // draw zeropoint vs mjd
-    function drawZPTChart() {
-        var data = new google.visualization.DataTable();
-        data.addColumn('number', 'MJD');
-
-        // Add columns for each filter type to handle separate colors
-        data.addColumn('number', 'g' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'r' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'i' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'z' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'y' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn('number', 'w' );
-        data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
-        data.addColumn({type: 'number', role: 'interval'});
-        data.addColumn({type: 'number', role: 'interval'});
-
-        // Count occurrences of each filter
-        const filterCounts = { g: 0, r: 0, i: 0, z: 0, y: 0, w: 0 };
-       const rows = dataArray.map(row => {
-            const [timestamp, mjd, filter, comment, ra, dec, exp_name, alt, az, iq_fwhm_maj, iq_fwhm_min, zpt_obs, zpt_stdev, exp_id, cam_label] = row;
-
-            // Initialize all filter values as null
-            let zp_g = null, zp_r = null, zp_i = null, zp_z = null, zp_y = null, zp_w = null;
-
-            // Increment the count for the filter
-            if (filterCounts.hasOwnProperty(filter[0])) {
-                filterCounts[filter[0]] += 1;
-            }
-
-            // Set the zeropoint offset based on filter and assign it to the correct series
-            switch (filter[0]) {
-                case 'g':
-                    zp_g = zpt_obs - 0.0;
-                    break;
-                case 'r':
-                    zp_r = zpt_obs - 0.0;
-                    break;
-                case 'i':
-                    zp_i = zpt_obs - 0.0;
-                    break;
-                case 'z':
-                    zp_z = zpt_obs - 0.0;
-                    break;
-                case 'y':
-                    zp_y = zpt_obs - 0.0;
-                    break;
-                case 'w':
-                    zp_w = zpt_obs - 0.0;
-                    break;
-            }
-
-            return [
-                parseFloat(mjd), // MJD value
-                zp_g, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_g - zpt_stdev), parseFloat(zp_g - zpt_stdev*-1),
-                zp_r, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_r - zpt_stdev), parseFloat(zp_r - zpt_stdev*-1),
-                zp_i, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_i - zpt_stdev), parseFloat(zp_i - zpt_stdev*-1),
-                zp_z, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_z - zpt_stdev), parseFloat(zp_z - zpt_stdev*-1),
-                zp_y, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_y - zpt_stdev), parseFloat(zp_y - zpt_stdev*-1),
-                zp_w, "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>HST: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,  
-parseFloat(zp_w - zpt_stdev), parseFloat(zp_w - zpt_stdev*-1),
-            ];
-        });
-      data.addRows(rows);
-
-        // Include counts in the title using string concatenation
-        const countsTitle = 'Zeropoint (MJD) g: ' + filterCounts.g + ', r: ' + filterCounts.r + ', i: ' + filterCounts.i + ', z: ' + filterCounts.z + ', y: ' + filterCounts.y + ', w: ' + filterCounts.w;
-
-
-        console.log(countsTitle);
-
-        const options = {
-            title: countsTitle,
-            titleTextStyle: { color: 'black', fontSize: 15 },
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#00f000', pointSize: 5, pointShape: 'circle',   linewidth: 1},  // 'g' filter
-                1: { color: '#f00000', pointSize: 5, pointShape: 'triangle', linewidth: 1},  // 'r' filter
-                2: { color: '#f05000', pointSize: 5, pointShape: 'square',   linewidth: 1},  // 'i' filter
-                3: { color: '#0050a0', pointSize: 5, pointShape: 'diamond',  linewidth: 1},  // 'z' filter
-                4: { color: '#ffe000', pointSize: 5, pointShape: 'star',     linewidth: 1},  // 'y' filter
-                5: { color: '#808080', pointSize: 5, pointShape: 'polygon',  linewidth: 1},  // 'w' filter
-            },
-            intervals: { color: 'red', lineWidth: 0, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: { count: -1 }
-            },
-            vAxis: {
-                title: 'Zeropoint (mag)'
-            },
-            tooltip: { isHtml: true },
-            chartArea: { left: '9%', top: 60, right: '5%', bottom: 50 },
-            fontSize: 12,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_zpt'));
-        chart.draw(data, options);
-    }
-
-    google.charts.setOnLoadCallback(drawFWHMChart);
-    google.charts.setOnLoadCallback(drawZPTChart);
-    google.charts.setOnLoadCallback(drawZOOMZPTChart);
-
-</script>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_fwhm" style="width:100%;"></div>
-    <div class="overlay" style="position: absolute; width: 400px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_zpt" style="width:100%;"></div>
-    <div class="overlay" style="position: absolute; width: 400px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_zoomzpt" style="width:100%;"></div>
-    <div class="overlay" style="position: absolute; width: 400px; bottom: 60px; left: 85px;"></div>
-</div>
-
-HTML;
-    // End timer
-    $total_time = round(microtime(true) - $start, 3);
-    echo "<center>loading plot in $total_time seconds<br></center>";
-
-}
-
-
-
 ?>
 
Index: /trunk/ippMonitor/raw/functions.php
===================================================================
--- /trunk/ippMonitor/raw/functions.php	(revision 42972)
+++ /trunk/ippMonitor/raw/functions.php	(revision 42972)
@@ -0,0 +1,1529 @@
+<?php
+// This should only load once for google charts below
+
+echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>
+<script> google.charts.load('current', {packages: ['corechart']}); </script>";
+
+###########################################################################
+#
+# Gets disk status
+#
+###########################################################################
+function getHosts($db) {
+    $start = microtime(true);
+    // Set environment variables
+    include_once __DIR__ . '/site.php';
+
+    global $PERLLIB, $BINDIR;
+
+    putenv("PERL5LIB=$PERLLIB");
+    putenv("PATH=$BINDIR:" . getenv('PATH'));
+
+    // Run external command
+    $hostnoteall = [];
+    exec('neb-host | cut -b 17-30,86-', $hostnoteall);
+
+    $sql = "SELECT host, format(total, 2), format(available, 2), format(used, 2), writable, readable, 
+                FORMAT(
+                    CASE 
+                        WHEN total = 0 THEN 100
+                        ELSE used / total * 100
+                    END, 3
+                ) AS ratio
+            FROM hosts where xattr <> 3 
+            ORDER BY CASE WHEN ratio = 100 THEN 0 ELSE 1 END, host;";
+
+    $qry = $db->query($sql);
+
+    if ($qry === false) {
+        $err = $db->errorInfo();
+        echo "<b>Error with SQL query:</b> " . htmlspecialchars($err[2]) . "<br>";
+        return;
+    }
+
+    $sqlMax = "SELECT MAX(total) AS max_total FROM hosts WHERE xattr <> 3";
+    $qryMax = $db->query($sqlMax);
+    $maxUsed = 0;
+
+    if ($qryMax) {
+        // single value
+        $maxUsed = $qryMax->fetchColumn();  // returns MAX(total)
+    }
+
+
+    $dataRows = [];
+    while ($row = $qry->fetch(PDO::FETCH_NUM)) {
+
+        // Access by index
+        $host      = $row[0]; // ipp071.0
+        $total     = $row[1]; // 80.0352 TB
+        $available = $row[2]; // 10.8096 TB
+        $used      = $row[3]; // 69.2256 TB
+        $writable  = $row[4]; // 0
+        $readable  = $row[5]; // 1
+        $ratio     = $row[6]; // 86.4939 %
+
+        // Match notes
+        $hostnotes = preg_grep("/$host/", $hostnoteall);
+        $hostnote = reset($hostnotes);
+
+        // Defaults
+        $styles    = "stroke-width: .5; stroke-color: black;";
+        $status    = "down";
+        $usedColor = "lightgrey";
+        $freeColor = "lightgrey";
+
+        // Status logic
+        if ($writable == 1 && $readable == 1) {
+            $status    = "up";
+            $usedColor = ($ratio >= 97) ? "pink" : "yellow"; // red if â¥97%
+            $freeColor = "#a6ec99"; // green free
+        } elseif ($writable == 0 && $readable == 1) {
+            $status    = "repair";
+            $usedColor = ($ratio >= 97) ? "pink" : "yellow";
+            $freeColor = "lightgrey"; // grey free
+        } elseif ($available < 1) {
+            $used      = $maxUsed;
+            $available = 0;
+            $status    = "offline";
+            $usedColor = "white";
+            $freeColor = "white";
+        }
+
+        $fontSize = "18px";
+        
+        if ($status == "down") {
+             $tooltipUsed = "<span style='font-size:$fontSize'>" . $host . ": " . round($ratio, 1) . "% of " . $total . " TB used " .
+                            "<span style='color:red; font-weight:bold;'> <br>" . $hostnote . "</span></span>";
+             $tooltipFree = "<span style='color:red; font-weight:bold; font-size:$fontSize'>" .
+                            $host . ": " . $available . " TB free" . "</span>";
+        } elseif ($status == "offline") {
+             $tooltipUsed = "<span style='font-size:$fontSize'>" . $host . ": " . round($ratio, 1) . "% of " . $total . " TB used " .
+                            "<span style='color:red; font-weight:bold;'> <br>" . $hostnote . "</span></span>";
+             $tooltipFree = "<span style='color:red; font-weight:bold; font-size:$fontSize'>" .
+                            $host . ": " . $total . " TB free" . "</span>";
+        } else {
+             $tooltipUsed = "<span style='font-size:$fontSize'>" .
+                            $host . ": " . round($ratio, 1) . "% of " . $total . " TB used<br>" . $hostnote . "</span>";
+             $tooltipFree = "<span style='font-size:$fontSize'>" .
+                            $host . ": " . $available . " TB free" . "</span>";
+        }
+
+        // Build data row (manual string concatenation for old PHP)
+        $dataRows[] =
+            "[ \"" . $host . ":" . $status . "\", " .
+            $used . ", \"" . $styles . " color: " . $usedColor . ";\", \"" . $tooltipUsed . "\", " .
+            $available . ", \"" . $styles . " color: " . $freeColor . ";\", \"" . $tooltipFree . "\" ]";
+
+    }
+    echo "<script type=\"text/javascript\">
+            google.charts.setOnLoadCallback(drawBarColors);
+
+            function drawBarColors() {
+                var data = google.visualization.arrayToDataTable([
+                    ['Host', 'Used', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}},
+                     'Free', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}}],
+                    " . implode(",\n", $dataRows) . "
+                ]);
+
+                var options = {
+                    title: 'Nebulous Disk Use across IPP Clusters',
+                    titleTextStyle: {color: 'black', fontSize: 15},
+                    width: '100%',
+                    height: '100%',
+                    tooltip: {isHtml: true},
+                    legend: { position: 'none', alignment: 'end', maxLines: 3 },
+                    hAxis: { title: 'Space (TB)', gridlines: {count: 20} },
+                    vAxis: { textStyle: {fontSize: 15}, format: 'string',  textPosition: 'in', direction: 1},
+                    bar: { groupWidth: '95%' },
+                    chartArea: {left:40, top:50, right:20, bottom:40},
+                    fontSize: 15,
+                    allowHtml: true,
+                    isStacked: true
+                };
+
+                var chart = new google.visualization.BarChart(document.getElementById('disk_div'));
+                chart.draw(data, options);
+            }
+          </script>";
+
+      echo "
+      <div class='chartWithOverlay' style='position: relative; width: 100%'>
+          <div id='disk_div' style='width:100%; height:5000px'></div>
+          <div class='overlay' style='position: absolute; width:160px; top:50px; right:30px; font-family:Arial, sans-serif;'>
+            <div style='font-size:15px; border:1px solid #888; background-color:lightgreen; color:black; text-align:center; margin-bottom:1px;'>Free</div>
+            <div style='font-size:15px; border:1px solid #888; background-color:yellow    ; color:black; text-align:center; margin-bottom:1px;'>Used</div>
+            <div style='font-size:15px; border:1px solid #888; background-color:pink      ; color:black; text-align:center; margin-bottom:1px;'>97% Usage</div>
+            <div style='font-size:15px; border:1px solid #888; background-color:lightgrey ; color:black; text-align:center; margin-bottom:1px;'>Down/Repair</div>
+            <div style='font-size:15px; border:1px solid #888; background-color:white     ; color:black; text-align:center; margin-bottom:1px;'>Offline</div>
+            <div style='font-size:15px; border:1px solid #888; background-color:#42a5f5   ; color:white; text-align:center; margin-bottom:1px;'>
+              <a href='https://atrcganglia.ifa.hawaii.edu/ganglia/' target='_blank' style='text-decoration:none; color:white; font-weight:bold;'>ATRC ganglia</a>
+            </div>
+          </div>
+      </div>";
+
+    $total_time = round(microtime(true) - $start, 3);
+    echo "<center>loading in $total_time seconds<br></center>";
+}
+
+
+###########################################################################
+#
+# Gets disk spaces status
+#
+###########################################################################
+function getSpaces(PDO $db, bool $debug = false) {
+    $start = microtime(true);
+
+    // helper for single-value queries
+    $getValue = function($sql) use ($db, $debug) {
+        if ($debug) echo htmlspecialchars($sql) . "<br>\n";
+        $stmt = $db->query($sql);
+        if (!$stmt) {
+            $err = $db->errorInfo();
+            echo "<b>Error with $sql:</b> " . $err[2] . "<br>\n";
+            return 0;
+        }
+        return $stmt->fetchColumn();
+    };
+
+    // compute values
+    $ippbusable = $getValue("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts WHERE host LIKE 'ippb%' AND xattr <> 3");
+    $ippbfree   = $getValue("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts WHERE host LIKE 'ippb%' AND writable = 1 AND xattr <> 3");
+    $ippusable  = $getValue("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts WHERE host NOT LIKE 'ippb%' AND xattr <> 3");
+    $ippfree    = $getValue("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts WHERE host NOT LIKE 'ippb%' AND writable = 1 AND xattr <> 3");
+    $bckusable  = $getValue("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts WHERE host LIKE 'ipp%_bck.0' AND xattr <> 3");
+    $bckfree    = $getValue("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts WHERE host LIKE 'ipp%_bck.0' AND writable = 1 AND xattr <> 3");
+
+    // get cluster space top row
+    $sql = "SELECT FORMAT(used/total*100,1), usable FROM cluster_space ORDER BY timestamp DESC LIMIT 1";
+    if ($debug) echo htmlspecialchars($sql) . "<br>\n";
+    $stmt = $db->query($sql);
+    [$allocated, $usable] = $stmt->fetch(PDO::FETCH_NUM);
+
+    // fetch cluster_space rows for chart
+    $interval = 10;
+    $sql = "SELECT sub.dated, sub.usable, sub.available
+            FROM (
+                SELECT DATE_FORMAT(timestamp, \"%Y.%m.%d.%H.%i\") AS dated, usable, available
+                FROM cluster_space
+                WHERE timestamp > CURDATE() - INTERVAL $interval DAY
+            ) AS sub
+            GROUP BY dated";
+    if ($debug) echo htmlspecialchars($sql) . "<br>\n";
+    $stmt = $db->query($sql);
+
+    $rowsJs = '';
+    while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+        $tsp = explode('.', $row[0]);
+        $mon = $tsp[1] - 1;
+        $rowsJs .= "[new Date($tsp[0], $mon, $tsp[2], $tsp[3], $tsp[4]), {$row[1]}, {$row[2]}],";
+    }
+
+    // build HTML + JS all at once
+    $output = <<<HTML
+    <script type="text/javascript">
+    google.charts.setOnLoadCallback(drawChart);
+    function drawChart() {
+        var data = new google.visualization.DataTable();
+        data.addColumn('datetime', 'Day');
+        data.addColumn('number', 'Usable (ipp:$ippusable, ippb:$ippbusable) TB');
+        data.addColumn('number', 'Free (ipp:$ippfree, ippb:$ippbfree) TB');
+        data.addRows([
+            $rowsJs
+        ]);
+    
+        var options = {
+            title: 'Total Available Cluster Space in the Past $interval Days ($allocated% of total allocated)',
+            height: 400,
+            legend: { position: 'top', alignment: 'center'},
+            lineWidth: 2,
+            series: {0:{color:'#0000ff',lineWidth:3},1:{color:'#33a532',lineWidth:3}},
+            hAxis: { title:'Date/Time (HST)', format:'MMM dd'},
+            vAxis: { title:'Available (TB)', viewWindow:{min:500}},
+            chartArea:{left:80,top:50,right:20,bottom:50},
+            fontSize:13
+        };
+        var chart = new google.visualization.LineChart(document.getElementById('space_div'));
+        chart.draw(data, options);
+    }
+    </script>
+
+    <div class="chartWithOverlay" style="position: relative; width: 100%; height:400px">
+      <div id="space_div" style="width:100%;"></div>
+      <div class="overlay" style="position: absolute; width: 200px; bottom: 60px; left: 80px;">
+        <div style="font-size: 16px; color:#0000ff;"><b>ipp*_bck: $bckusable TB (usable)</b></div>
+        <div style="font-size: 16px; color:#33a532;"><b>ipp*_bck: $bckfree TB (free)</b></div>
+      </div>
+    </div>
+HTML;
+
+    echo $output;
+    $total_time = round(microtime(true) - $start, 3);
+    echo "<center>loading in $total_time seconds<br></center>";
+}
+
+// Global helper function (loaded once)
+echo <<<HTML
+<script type="text/javascript">
+function buildTooltip(row) {
+    return `
+        expname        : \${row.exp_name} <br>
+        exp_id         : \${row.exp_id} <br>
+        dateobs        : \${row.time} (HST)<br>
+        (RA, Dec)      : \${row.RA}, \${row.DECL} (deg)<br>
+        filter         : \${row.filter}<br>
+        airmass        : \${row.airmass}<br>
+        (RA_off, scat) : \${row.AST_R0}, \${row.AST_RS} (TP)<br>
+        (DEC_off, scat): \${row.AST_D0}, \${row.AST_DS} (TP)<br>
+        zp, stdev      : \${row.zpt_obs}, \${row.zpt_stdev}<br>
+        pa, alt, az    : \${row.posang}, \${row.alt}, \${row.az}<br>
+        exp_time       : \${row.exp_time}(s)<br>
+        cam_label      : \${row.label}<br>
+        cam_id         : \${row.cam_id}<br>
+        moon_angle     : \${row.moon_angle}<br>
+        raw_chips      : \${row.n_raw}<br>
+        good_chips     : \${row.g_chips}<br>
+        comment        : \${row.comment}<br>
+    `;
+}
+</script>
+HTML;
+
+
+###########################################################################
+#
+# Get pointing
+#
+###########################################################################
+function getPointing($db, $range = 'week', $proj) {
+    // Start timer
+    $start = microtime(true);
+
+    // testing day
+    //$mjdDay = 60945;
+    $mjdDay  = getMJD();
+    $mjdDay_yearback = $mjdDay - 366;
+    $mjdDay_monthback = $mjdDay - 32;
+    $min_exp_id = 0;
+
+
+    // Determine the reference exp_name based on the selected range
+    switch ($range) {
+        case 'year':
+            $expname = "o" . $mjdDay_yearback . "%";
+            break;
+        case 'month':
+            $expname = "o" . $mjdDay_monthback . "%";
+            break;
+        case 'week':
+            $expname = "o" . ($mjdDay - 7) . "%";
+            break;
+        case 'today':
+            $expname = "o" . $mjdDay . "%";
+            break;
+        case 'yesterday':
+            $expname = "o" . ($mjdDay - 1) . "%";
+            break;
+        default:
+            $expname = "o" . ($mjdDay - 7) . "%"; // Default: week
+            break;
+    }
+
+    // --- PDO query for min_exp_id ---
+    $stmt = $db->prepare("SELECT MIN(exp_id) AS min_exp_id FROM rawExp WHERE exp_name LIKE :expname");
+    $stmt->execute([':expname' => $expname]);
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+//    if (!empty($row['min_exp_id'])) {
+//        $min_exp_id = $row['min_exp_id'];
+//    } else {
+//        echo "<center><b>No results found for min_exp_id</b></center>";
+//    }
+
+    if (!empty($row['min_exp_id'])) {
+        $min_exp_id = $row['min_exp_id'];
+        echo "<center><b>min_exp_id:$min_exp_id $mjdDay_monthback $mjdDay</b></center>";
+    } else {
+        echo "<center><b>No results found for min_exp_id:$min_exp_id $mjdDay_monthback $mjdDay</b></center>";
+    }
+
+    //echo "<br><br>";
+
+    // Determine the date constraint based on the range
+    switch ($range) {
+        case 'year':
+            $dateConstraint = "dateobs >= NOW() - INTERVAL 365 DAY AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'month':
+            $dateConstraint = "dateobs >= NOW() - INTERVAL 30 DAY AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'week':
+            $expnames = array();
+            for ($i = 0; $i < 7; $i++) {
+                $expnames[] = "rawExp.exp_name LIKE 'o" . ($mjdDay - $i) . "%'";
+            }
+            $dateConstraint = "(" . implode(" OR ", $expnames) . ") AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'yesterday':
+            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'today':
+            $dateConstraint = "rawExp.exp_name LIKE 'o$mjdDay%' AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'all':
+            $dateConstraint = "rawExp.exp_id >= 0";
+            break;
+        default: // Default to last night
+            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
+            break;
+    }
+ 
+    // Updated SQL query
+    $sql = "select * from (SELECT SUBTIME(dateobs, '10:00:00') AS time, AST_R0, AST_D0, AST_RS, AST_DS, AST_T0, posang,
+                   round(( 367*YEAR(dateobs)- FLOOR((7 * (YEAR(dateobs) + FLOOR((MONTH(dateobs) + 9) / 12))) / 4) + FLOOR(275 * MONTH(dateobs) / 9)
+                   + DAY(dateobs) + 1721013.5 + (HOUR(dateobs) / 24.0) + (MINUTE(dateobs) / 1440.0) + (SECOND(dateobs) / 86400.0) - 2400000.5), 5) AS MJD,
+                   filter, comment, round(ra*180/pi(), 5) as RA, round(decl*180/pi(), 5) as DECL, exp_name, alt, 
+                   az, fwhm_major, fwhm_minor, zpt_obs, zpt_stdev, airmass, exp_time, 
+                   rawExp.exp_id, camRun.label, moon_angle, cam_id, BIT_COUNT(astrom_chips) AS working_chips
+            FROM rawExp
+            JOIN chipRun USING (exp_id)
+            JOIN camRun USING (chip_id)
+            JOIN camProcessedExp USING (cam_id)
+            WHERE $dateConstraint
+              AND exp_type = 'OBJECT'
+              AND camRun.state LIKE 'full'
+              AND camProcessedExp.fault = 0
+              AND camProcessedExp.zpt_obs != 0 ORDER BY RAND() limit 10000) as sub order by time;";
+
+    $stmt = $db->query($sql);
+    $DQarray = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    
+    // Ensure $DQarray is not empty
+    if (empty($DQarray)) {
+        echo "<center><b> No data for MJD: $mjdDay </b></center>";
+        return; // Stop execution if no data to encode
+    }
+    
+    // Convert data array to JSON for easier handling in JavaScript
+    $dataJson = json_encode($DQarray);
+    
+    if ($dataJson === false) {
+        echo "<b>Error encoding JSON:</b> " . json_last_error_msg(); // Display any JSON encoding errors
+        return; // Stop execution if JSON encoding fails
+    }
+    
+    echo "<script>console.log(" . json_encode($dataJson) . ");</script>"; // Log JSON output to console for debugging
+
+
+    echo <<<HTML
+    <script type="text/javascript">
+    // Use the encoded JSON directly in JavaScript
+    const dataArray = JSON.parse('{$dataJson}'); // Directly parse the PHP JSON string
+
+    const nRows = dataArray.length;
+
+    // draw pointing vs mjd
+    function drawMJDChart() {
+        var data = new google.visualization.DataTable();
+            data.addColumn('number', 'MJD');
+            data.addColumn('number', 'RA_offset (AST_R0)');
+            data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
+            data.addColumn({type: 'number', role: 'interval'});
+            data.addColumn({type: 'number', role: 'interval'});
+            data.addColumn('number', 'Dec_offset (AST_D0)');
+            data.addColumn({type: 'string', role: 'tooltip', 'p': {'html': true}});
+            data.addColumn({type: 'number', role: 'interval'});
+            data.addColumn({type: 'number', role: 'interval'});
+            data.addColumn('number', '20 TP unit ~5.1"');
+            data.addColumn('number', '-20 TP unit ~5.1"');
+
+	const mjdValues = dataArray.map(r => parseFloat(r.MJD));
+        let mjdMin0 = Math.min(...mjdValues);
+        const mjdMin = Math.floor(mjdMin0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        let mjdMax0 = Math.max(...mjdValues);
+        const mjdMax = Math.ceil(mjdMax0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+	// Extract the first MJD value from dataArray
+        const firstMJD = dataArray.length > 0 ? Math.floor(parseFloat(dataArray[0].MJD)) : "N/A";
+        console.log("First MJD Value:", firstMJD);
+        const mjdoff = mjdMin;
+
+	const mjdVals = [];
+	const rows = dataArray.map(row => {
+            const mjd     = parseFloat(row.MJD) - mjdoff; // MJD shift to first one
+            mjdVals.push(mjd);  // collect MJD offset values
+            const ra_off  = parseFloat(row.AST_R0);  // RA_offset (AST_R0)
+            const dec_off = parseFloat(row.AST_D0);  // DEC_offset (AST_D0)
+            const ra_sct  = parseFloat(row.AST_RS);  // RA_scatter (AST_RS)
+            const dec_sct = parseFloat(row.AST_DS);  // DEC_scatter (AST_DS)
+            const tooltip  = buildTooltip(row);      // Tooltip
+
+	    return [
+              mjd, ra_off, tooltip, ra_off-ra_sct, ra_off+ra_sct, dec_off, tooltip, dec_off-dec_sct, dec_off+dec_sct, 
+              20,   // Dummy value for 20 TP unit
+              -20  // Dummy value for -20 TP unit
+	    ];
+        });
+
+        data.addRows(rows);
+
+        // After building rows:
+        const allVerticalVals = rows.flatMap(r => [
+          r[3], // ra_off - ra_sct
+          r[4], // ra_off + ra_sct
+          r[7], // dec_off - dec_sct
+          r[8]  // dec_off + dec_sct
+        ]).filter(v => !isNaN(v)); // â
+ remove NaN values
+        
+        // If array is empty after filtering, use safe defaults
+        const minY = allVerticalVals.length ? Math.min(...allVerticalVals) : 0;
+        const maxY = allVerticalVals.length ? Math.max(...allVerticalVals) : 0;
+        
+        // Clamp values: use Â±100 if beyond 100
+        const yMin = Math.abs(minY) > 150 ? -150 : minY;
+        const yMax = Math.abs(maxY) > 150 ?  150 : maxY;
+        
+        console.log("minY:", minY, yMin, "maxY:", maxY, yMax);
+
+        // Now compute min and max
+        const mjdMin1 = Math.min(...mjdVals);
+        const mjdMax1 = Math.max(...mjdVals);
+
+        const defaultSeries = { pointSize: 5, lineWidth: 0 };
+
+        const titleText = `Pointing Offset vs. MJD ({$proj}) â \${nRows} rows\${dataArray.length >= 10000 ? " (sampled)" : ""}`;
+        const options = {
+            //title: 'Pointing Offset vs. MJD ({$proj})',
+            title: titleText,
+            titleTextStyle: {color: 'black', fontSize: 15},
+            width: '100%',
+            height: 480,
+            legend: { position: 'top', alignment: 'center' },
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
+                2: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }, // Line at y = 20
+                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }  // Line at y = -20
+            },
+            intervals: { lineWidth: .5, style: 'sticks' },
+            interval: {
+              0: {color: 'blue'},    // interval columns attached to first series
+              1: {color: 'green'}    // interval columns attached to second series
+            },
+	    hAxis: {
+                title: 'MJD - ' + mjdoff + ' (day)',
+                viewWindow: {
+                   min: 0.,  // small padding
+                   max: mjdMax1 + 0.005
+                },
+               gridlines: { count: -1 }  // auto gridlines
+            },
+            vAxis: {
+              title: 'Pointing Offset (TP units)',
+              viewWindow: {
+                min: yMin,
+                max: yMax
+              }
+            },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
+            fontSize: 15,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_mjd'));
+        chart.draw(data, options);
+    }
+
+    // position angle
+    function drawPOSChart() {
+        var data = new google.visualization.DataTable();
+            data.addColumn('number', 'Posang');
+            data.addColumn('number', 'RA_offset (AST_R0)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+            data.addColumn('number', 'Dec_offset (AST_D0)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+
+	const rows = dataArray.map(row => {
+            const posang  = parseFloat(row.posang);  // Position Angle
+            const ra_off  = parseFloat(row.AST_R0);  // RA_offset (AST_R0)
+            const dec_off = parseFloat(row.AST_D0);  // DEC_offset (AST_D0)
+            const tooltip  = buildTooltip(row);      // Tooltip
+	    return [
+              posang, ra_off, tooltip, dec_off, tooltip
+	    ];
+        });
+
+        data.addRows(rows);
+
+        const titleText = `Pointing Offset vs. Position Angle ({$proj}) â \${nRows} rows\${dataArray.length >= 10000 ? " (sampled)" : ""}`;
+        const options = {
+            title: titleText,
+            titleTextStyle: {color: 'black', fontSize: 15},
+            width: '100%',
+            height: 480,
+            legend: { position: 'top', alignment: 'center' },
+            // pointSize: 5, // Point size for scatter plot
+            // lineWidth: 2,
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
+            },
+            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
+            hAxis: {
+                title: 'Position Angle (deg)',
+                gridlines: {count: -1}
+            },
+             vAxis: {
+                 title: 'Pointing Offset (TP units)',
+             },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
+            fontSize: 15,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_pos'));
+        chart.draw(data, options);
+    }
+
+    function drawALTChart() {
+        var data = new google.visualization.DataTable();
+            data.addColumn('number', 'Altitute');
+            data.addColumn('number', 'RA_offset (AST_R0)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+            data.addColumn('number', 'Dec_offset (AST_D0)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+
+	const rows = dataArray.map(row => {
+            const alt     = parseFloat(row.alt);     // Altitude
+            const ra_off  = parseFloat(row.AST_R0);  // RA_offset (AST_R0)
+            const dec_off = parseFloat(row.AST_D0);  // DEC_offset (AST_D0)
+	    const tooltip  = buildTooltip(row);      // Tooltip
+	    return [
+              alt, ra_off, tooltip, dec_off, tooltip
+	    ];
+        });
+
+        data.addRows(rows);
+
+        const titleText = `Pointing Offset vs. Altitude ({$proj}) â \${nRows} rows\${dataArray.length >= 10000 ? " (sampled)" : ""}`;
+        const options = {
+            title: titleText,
+            titleTextStyle: {color: 'black', fontSize: 15},
+            width: '100%',
+            height: 480,
+            legend: { position: 'top', alignment: 'center' },
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
+            },
+            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
+            hAxis: {
+                title: 'Altitude (deg)',
+                gridlines: {count: -1}
+            },
+            vAxis: {
+                title: 'Pointing Offset (TP units)',
+            },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            //chartArea: {left: 80, top: 50, right: 20, bottom: 50},
+            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
+            fontSize: 15,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_alt'));
+        chart.draw(data, options);
+    }
+
+   function drawAZChart() {
+        var data = new google.visualization.DataTable();
+            data.addColumn('number', 'Azumith');
+            data.addColumn('number', 'RA_offset (AST_R0)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+            data.addColumn('number', 'Dec_offset (AST_D0)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+
+	const rows = dataArray.map(row => {
+            const az      = parseFloat(row.az);      // Azumith
+            const ra_off  = parseFloat(row.AST_R0);  // RA_offset (AST_R0)
+            const dec_off = parseFloat(row.AST_D0);  // DEC_offset (AST_D0)
+	    const tooltip  = buildTooltip(row);      // Tooltip
+	    return [
+              az, ra_off, tooltip, dec_off, tooltip
+	    ];
+        });
+
+        data.addRows(rows);
+
+        const titleText = `Pointing Offset vs. Azumith ({$proj}) â \${nRows} rows\${dataArray.length >= 10000 ? " (sampled)" : ""}`;
+        const options = {
+            title: titleText,
+            titleTextStyle: {color: 'black', fontSize: 15},
+            width: '100%',
+            height: 480,
+            legend: { position: 'top', alignment: 'center' },
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
+            },
+            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
+            hAxis: {
+                title: 'Azumith (deg)',
+                gridlines: {count: -1}
+            },
+            vAxis: {
+                title: 'Pointing Offset (TP units)',
+            },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            //chartArea: {left: 80, top: 50, right: 20, bottom: 50},
+            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
+            fontSize: 15,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_az'));
+        chart.draw(data, options);
+    }
+
+
+   function drawScatChart() {
+        var data = new google.visualization.DataTable();
+            data.addColumn('number', 'tmp');
+            data.addColumn('number', 'RA_scat (AST_RS)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+            data.addColumn('number', 'Dec_scat (AST_DS)');
+            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+
+	const rows = dataArray.map(row => {
+            const par  = parseFloat(row.MJD); 
+            const ra_scat  = parseFloat(row.AST_RS);  // RA_scat (AST_RS)
+            const dec_scat = parseFloat(row.AST_DS);  // DEC_scat (AST_DS)
+	    const tooltip  = buildTooltip(row);       // Tooltip
+	    return [
+              par, ra_scat, tooltip, dec_scat, tooltip
+	    ];
+        });
+
+        data.addRows(rows);
+
+        const titleText = `Pointing Offset vs. MJD ({$proj}) â \${nRows} rows\${dataArray.length >= 10000 ? " (sampled)" : ""}`;
+        const options = {
+            title: titleText,
+            titleTextStyle: {color: 'black', fontSize: 15},
+            width: '100%',
+            height: 480,
+            legend: { position: 'top', alignment: 'center' },
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
+            },
+            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
+            hAxis: {
+                title: 'MJD',
+                gridlines: {count: -1}
+            },
+            vAxis: {
+                title: 'Pointing Scatter (TP units)',
+              viewWindow: {
+                min: 0,
+               // max: 20
+              }
+            },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            //chartArea: {left: 80, top: 50, right: 20, bottom: 50},
+            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
+            fontSize: 15,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_scat'));
+        chart.draw(data, options);
+    }
+
+    google.charts.setOnLoadCallback(drawMJDChart);
+    google.charts.setOnLoadCallback(drawPOSChart);
+    google.charts.setOnLoadCallback(drawALTChart);
+    google.charts.setOnLoadCallback(drawAZChart);
+    google.charts.setOnLoadCallback(drawScatChart);
+</script>
+
+<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
+    <div id="dq_div_mjd" style="width:1240px;"></div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
+    <div id="dq_div_scat" style="width:1240px;"></div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
+    <div id="dq_div_pos" style="width:1240px;"></div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
+    <div id="dq_div_alt" style="width:1240px;"></div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
+    <div id="dq_div_az" style="width:1240px;"></div>
+</div>
+HTML;
+
+// End timer
+$total_time = round(microtime(true) - $start, 3);
+echo "<center>Loading plot in $total_time seconds<br></center>";
+
+}
+
+###########################################################################
+#
+# Get data quality
+#
+###########################################################################
+function getDataQuality($db, $range = 'week', $proj) {
+    // Start timer
+    $start = microtime(true);
+
+    // testing day
+    $mjdDay  = getMJD();
+    //$mjdDay = 60950;
+    $mjdDay_yearback = $mjdDay - 366;
+    $mjdDay_monthback = $mjdDay - 32;
+    $min_exp_id = 0;
+
+
+    // Determine the reference exp_name based on the selected range
+    switch ($range) {
+        case 'year':
+            $expname = "o" . $mjdDay_yearback . "%";
+            break;
+        case 'month':
+            $expname = "o" . $mjdDay_monthback . "%";
+            break;
+        case 'week':
+            $expname = "o" . ($mjdDay - 7) . "%";
+            break;
+        case 'yesterday':
+            $expname = "o" . ($mjdDay - 1) . "%";
+            break;
+        case 'today':
+            $expname = "o" . $mjdDay . "%";
+            break;
+        default:
+            $expname = "o" . ($mjdDay - 7) . "%"; // Default: week
+            break;
+    }
+
+    // --- PDO query for min_exp_id ---
+    $stmt = $db->prepare("SELECT MIN(exp_id) AS min_exp_id FROM rawExp WHERE exp_name LIKE :expname");
+    $stmt->execute([':expname' => $expname]);
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (!empty($row['min_exp_id'])) {
+        $min_exp_id = $row['min_exp_id'];
+        echo "<center><b>min_exp_id:$min_exp_id $mjdDay_monthback $mjdDay</b></center>";
+    } else {
+        echo "<center><b>No results found for min_exp_id:$min_exp_id $mjdDay_monthback $mjdDay</b></center>";
+    }
+
+    // Determine the date constraint based on the range
+    switch ($range) {
+        case 'year':
+            $dateConstraint = "rawExp.dateobs >= NOW() - INTERVAL 365 DAY AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'month':
+            $dateConstraint = "rawExp.dateobs >= NOW() - INTERVAL 30 DAY AND rawExp.exp_id >= $min_exp_id";
+            break;
+        case 'week':
+            $expnames = array();
+            for ($i = 0; $i < 7; $i++) {
+                $expnames[] = "rawExp.exp_name LIKE 'o" . ($mjdDay - $i) . "%'";
+            }
+            $dateConstraint = "(" . implode(" OR ", $expnames) . ") AND rawExp.exp_id >= $min_exp_id";
+            $qday = $mjdDay - 6; // or something meaningful for week
+            break;
+        case 'yesterday':
+            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
+	    $qday = $mjdDay - 1;
+            break;
+        case 'today':
+            $dateConstraint = "rawExp.exp_name LIKE 'o$mjdDay%' AND rawExp.exp_id >= $min_exp_id";
+	    $qday = $mjdDay;
+            break;
+        case 'all':
+            $dateConstraint = "rawExp.exp_id >= 0";
+            break;
+        default: // Default to last night
+            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
+	    $qday = $mjdDay - 1;
+            break;
+    }
+
+    // Updated SQL query
+    $sql1 = "select * from (SELECT SUBTIME(dateobs, '10:00:00') AS time, AST_R0, AST_D0, AST_RS, AST_DS, AST_T0, posang,
+                   round(( 367*YEAR(dateobs)- FLOOR((7 * (YEAR(dateobs) + FLOOR((MONTH(dateobs) + 9) / 12))) / 4) + FLOOR(275 * MONTH(dateobs) / 9)
+                   + DAY(dateobs) + 1721013.5 + (HOUR(dateobs) / 24.0) + (MINUTE(dateobs) / 1440.0) + (SECOND(dateobs) / 86400.0) - 2400000.5), 5) AS MJD,
+                   filter, comment, round(ra*180/pi(), 5) as RA, round(decl*180/pi(), 5) as DECL, exp_name, alt, 
+                   az, fwhm_major, fwhm_minor, zpt_obs, zpt_stdev, airmass, exp_time, 
+                   rawExp.exp_id, camRun.label, moon_angle, cam_id, BIT_COUNT(astrom_chips) AS working_chips
+            FROM rawExp 
+            JOIN chipRun USING (exp_id) 
+            JOIN camRun USING (chip_id) 
+            JOIN camProcessedExp USING (cam_id)
+            WHERE $dateConstraint
+              AND exp_type = 'OBJECT'
+              AND camRun.state LIKE 'full'
+              AND camProcessedExp.fault = 0
+              AND camProcessedExp.zpt_obs != 0 ORDER BY RAND() limit 5000) as sub order by time;";
+    $sql = " SELECT * FROM (SELECT
+    SUBTIME(rawExp.dateobs, '10:00:00') AS time, 
+    cme.AST_R0, 
+    cme.AST_D0, 
+    cme.AST_RS, 
+    cme.AST_DS,  
+    cme.AST_T0, 
+    rawExp.posang,
+    ROUND(
+        (367*YEAR(rawExp.dateobs)
+        - FLOOR((7*(YEAR(rawExp.dateobs)+FLOOR((MONTH(rawExp.dateobs)+9)/12)))/4)
+        + FLOOR(275*MONTH(rawExp.dateobs)/9)
+        + DAY(rawExp.dateobs)
+        + 1721013.5
+        + (HOUR(rawExp.dateobs)/24.0)
+        + (MINUTE(rawExp.dateobs)/1440.0)
+        + (SECOND(rawExp.dateobs)/86400.0)
+        - 2400000.5),5
+    ) AS MJD,
+    rawExp.filter,
+    rawExp.comment,
+    ROUND(rawExp.ra*180/PI(),5) AS RA,
+    ROUND(rawExp.decl*180/PI(),5) AS DECL,
+    rawExp.exp_name,
+    rawExp.alt,
+    rawExp.az,
+    cme.fwhm_major, 
+    cme.fwhm_minor, 
+    cme.zpt_obs, 
+    cme.zpt_stdev, 
+    rawExp.airmass, 
+    rawExp.exp_time,
+    rawExp.exp_id,
+    rawExp.moon_angle,
+    camRun.label,
+    camRun.cam_id,
+    BIT_COUNT(astrom_chips) AS g_chips,
+    SUM(CASE WHEN rim.bg > 0 THEN 1 ELSE 0 END) AS n_raw,
+    SUM(CASE WHEN chi.bg IS NOT NULL THEN 1 ELSE 0 END) AS n_read,
+    SUM(CASE WHEN chi.quality > 0 THEN 1 ELSE 0 END) AS n_poor
+
+FROM camProcessedExp cme
+JOIN camRun USING (cam_id)
+JOIN chipRun USING (chip_id)
+JOIN chipProcessedImfile chi USING (chip_id)
+JOIN rawExp ON (rawExp.exp_id = chipRun.exp_id)
+JOIN rawImfile rim ON (rim.exp_id = rawExp.exp_id)
+
+WHERE
+    $dateConstraint
+    AND rawExp.exp_type = 'OBJECT'
+    AND camRun.state = 'full'
+    AND cme.fault = 0
+    AND cme.zpt_obs != 0
+    AND rim.class_id = chi.class_id
+
+GROUP BY
+    rawExp.exp_name,
+    camRun.label,
+    cam_id,
+    rawExp.dateobs,
+    rawExp.ra,
+    rawExp.decl,
+    rawExp.alt,
+    rawExp.moon_angle,
+    astrom_chips,
+    cme.bg,
+    cme.n_stars
+
+ORDER BY RAND() limit 5000) as sub order by time;
+";
+
+//echo "<center>$sql</center><br>";
+//echo "<center>$sql1</center>";
+    $stmt = $db->query($sql);
+    $DQarray = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+    // Ensure $DQarray is not empty
+    if (empty($DQarray)) {
+        echo "<center><b> No data for MJD: $qday </b></center>";
+        // End timer
+        $total_time = round(microtime(true) - $start, 3);
+        echo "<center>loading plot in $total_time seconds<br><br></center>";
+        return; // Stop execution if no data to encode
+    }
+
+    // Convert data array to JSON for easier handling in JavaScript
+    $dataJson = json_encode($DQarray);
+
+    if ($dataJson === false) {
+        echo "<b>Error encoding JSON:</b> " . json_last_error_msg(); // Display any JSON encoding errors
+        // End timer
+        $total_time = round(microtime(true) - $start, 3);
+        echo "<center>loading plot in $total_time seconds<br><br></center>";
+        return; // Stop execution if JSON encoding fails
+    }
+
+    echo "<script>console.log(" . json_encode($dataJson) . ");</script>"; // Log JSON output to console for debugging
+    
+    // start for Google Chart plot.   
+    echo <<<HTML
+
+    <script type="text/javascript">
+    // Use the encoded JSON directly in JavaScript
+    const dataArray = JSON.parse('{$dataJson}'); // Directly parse the PHP JSON string
+
+    const nRows = dataArray.length;
+
+    // draw working chips vs MJD
+    function drawCHIPChart() {
+
+	// data header
+        var data = new google.visualization.DataTable();
+        data.addColumn('number', 'MJD');
+        data.addColumn('number', 'Number of raw Chips');
+        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+//        data.addColumn('number', 'Number of Good Chips');
+//        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+
+        const mjdValues = dataArray.map(r => parseFloat(r.MJD));
+        let mjdMin0 = Math.min(...mjdValues);
+	const mjdMin = Math.floor(mjdMin0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        let mjdMax0 = Math.max(...mjdValues);
+	const mjdMax = Math.ceil(mjdMax0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        // Extract the first MJD value from dataArray
+        const firstMJD = dataArray.length > 0 ? Math.floor(parseFloat(dataArray[0].MJD)) : "N/A";
+	const mjdoff = mjdMin;
+	// output at console
+        // console.log("First MJD Value:", firstMJD);
+
+	// build data rows
+	const mjdVals = [];
+        const rows = dataArray.map(row => {
+	    const mjd    = parseFloat(row.MJD) - mjdoff;
+            mjdVals.push(mjd);  // collect MJD offset values
+
+  	    const n_raw   = parseFloat(row.n_raw)+1;
+  	    const n_chips = parseFloat(row.g_chips);
+  	    const f_good  = n_chips / n_raw;
+            const tooltip = buildTooltip(row);
+
+	    return[
+                mjd, f_good, tooltip, 
+            ];
+	});
+
+        data.addRows(rows);
+
+	// Now compute min and max
+        const mjdMin1 = Math.min(...mjdVals);
+        const mjdMax1 = Math.max(...mjdVals);
+
+        const titleText = `N Chips vs. MJD ({$proj}) â \${nRows} rows\${dataArray.length >= 5000 ? " (sampled)" : ""}`;
+
+        const options = {
+            title: titleText, 
+            titleTextStyle: {color: 'black', fontSize: 14, bold: true},
+            width:  '100%',
+            height: Math.min(window.innerHeight * 0.6, 430), // This gives a nice result on smaller screens.
+            legend: { position: 'top', alignment: 'center' },
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0. },
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0. },
+                2: { color: 'red', lineWidth: 2, pointSize: 0 },
+                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }
+            },
+            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
+            hAxis: {
+		title: 'MJD - ' + mjdoff + ' (day)',
+	        viewWindow: {
+	           min: 0.,  // small padding
+                   max: mjdMax1 + 0.005
+                },
+	       gridlines: { count: -1 }  // auto gridlines
+            },
+            vAxis: {
+                title: 'Good chips (number)',
+	    //    viewWindow: {
+            //       max: 65,
+            //       min: 0
+            //    },
+            },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            chartArea: {left: '9%', top: '10%', right: '3%', bottom: 50}, // Adjust left and right
+            fontSize: 14,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_chips'));
+        chart.draw(data, options);
+    }
+
+    // draw FWHM vs MJD
+    function drawFWHMChart() {
+
+	// data header
+        var data = new google.visualization.DataTable();
+        data.addColumn('number', 'MJD');
+        data.addColumn('number', 'FWHM_maj');
+        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+        data.addColumn('number', 'FWHM_min');
+        data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
+        data.addColumn('number', '1"');
+        data.addColumn('number', '3"');
+
+        const mjdValues = dataArray.map(r => parseFloat(r.MJD));
+        let mjdMin0 = Math.min(...mjdValues);
+	const mjdMin = Math.floor(mjdMin0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        let mjdMax0 = Math.max(...mjdValues);
+	const mjdMax = Math.ceil(mjdMax0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        // Extract the first MJD value from dataArray
+        const firstMJD = dataArray.length > 0 ? Math.floor(parseFloat(dataArray[0].MJD)) : "N/A";
+	const mjdoff = mjdMin;
+	// output at console
+        // console.log("First MJD Value:", firstMJD);
+
+	// build data rows
+	const mjdVals = [];
+        const rows = dataArray.map(row => {
+	    const mjd    = parseFloat(row.MJD) - mjdoff;
+            mjdVals.push(mjd);  // collect MJD offset values
+
+	    const fwhm_maj = parseFloat(row.fwhm_major);
+	    const fwhm_min = parseFloat(row.fwhm_minor);
+            const tooltip  = buildTooltip(row);
+
+	    return[
+                mjd, fwhm_maj, tooltip, fwhm_min, tooltip, 4, 12,
+            ];
+	});
+
+        data.addRows(rows);
+
+	// Now compute min and max
+        const mjdMin1 = Math.min(...mjdVals);
+        const mjdMax1 = Math.max(...mjdVals);
+
+        const titleText = `FWHM vs. MJD ({$proj}) â \${nRows} rows\${dataArray.length >= 5000 ? " (sampled)" : ""}`;
+
+        const options = {
+            title: titleText, 
+            titleTextStyle: {color: 'black', fontSize: 14, bold: true},
+            width:  '100%',
+            height: Math.min(window.innerHeight * 0.6, 430), // This gives a nice result on smaller screens.
+            legend: { position: 'top', alignment: 'center' },
+            series: {
+                0: { color: '#0000ff', pointSize: 3, lineWidth: 0. },
+                1: { color: '#33a532', pointSize: 3, lineWidth: 0. },
+                2: { color: 'red', lineWidth: 2, pointSize: 0 },
+                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }
+            },
+            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
+            hAxis: {
+		title: 'MJD - ' + mjdoff + ' (day)',
+	        viewWindow: {
+	           min: 0.,  // small padding
+                   max: mjdMax1 + 0.005
+                },
+	       gridlines: { count: -1 }  // auto gridlines
+            },
+            vAxis: {
+                title: 'FWHM (pixels)',
+            },
+            tooltip: { isHtml: true }, // Render tooltips as HTML
+            chartArea: {left: '9%', top: '10%', right: '3%', bottom: 50}, // Adjust left and right
+            fontSize: 14,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_fwhm'));
+        chart.draw(data, options);
+    }
+
+    function drawZPTChart() {
+        let gzp0, rzp0, izp0, zzp0, yzp0, wzp0; // declare outside
+        switch ('{$proj}') {
+            case 'gpc1':
+              // ref?
+              gzp0 = 24.563; rzp0 = 24.750; izp0 = 24.611; zzp0 = 24.240; yzp0 = 23.320; wzp0 = 26.000;
+              // 
+              //gzp0 = 24.563; rzp0 = 24.750; izp0 = 24.611; zzp0 = 24.240; yzp0 = 23.320; wzp0 = 26.000;
+              break;
+            case 'gpc2':
+              // ref?
+              gzp0 = 25.013; rzp0 = 28.840; izp0 = 24.711; zzp0 = 24.300; yzp0 = 23.570; wzp0 = 26.000;
+              break;
+        }
+        const filters = [
+            { key: 'g', zp: gzp0, color: '#00f000', shape: 'circle' },
+            { key: 'r', zp: rzp0, color: '#f00000', shape: 'triangle' },
+            { key: 'i', zp: izp0, color: '#f05000', shape: 'square' },
+            { key: 'z', zp: zzp0, color: '#0050a0', shape: 'diamond' },
+            { key: 'y', zp: yzp0, color: '#ffe000', shape: 'star' },
+            { key: 'w', zp: wzp0, color: '#808080', shape: 'polygon' }
+        ];
+    
+        // Count occurrences of each filter
+        const filterCounts = Object.fromEntries(filters.map(f => [f.key, 0]));
+    
+        const mjdValues = dataArray.map(r => parseFloat(r.MJD));
+        const mjdMin0 = Math.min(...mjdValues);
+        const mjdMax0 = Math.max(...mjdValues);
+        const mjdMin = Math.floor(mjdMin0 * 10) / 10;
+        const mjdMax = Math.ceil(mjdMax0 * 10) / 10;
+        const mjdoff = mjdMin;
+    
+        const mjdVals = [];
+    
+        // build rows
+        const rows = dataArray.map(row => {
+            const mjd = parseFloat(row.MJD) - mjdoff;
+            mjdVals.push(mjd);
+    
+            const filter = row.filter[0];
+            const zpt_obs = parseFloat(row.zpt_obs);
+            const zpt_stdev = parseFloat(row.zpt_stdev);
+    
+            // Initialize one slot per filter
+            const zpVals = {};
+            filters.forEach(f => zpVals[f.key] = null);
+    
+            if (filterCounts.hasOwnProperty(filter)) {
+                filterCounts[filter] += 1;
+                zpVals[filter] = zpt_obs - 0.0; // offset if needed
+            }
+    
+            const tooltip  = buildTooltip(row);  // Tooltips
+
+            return [
+                mjd,
+                ...filters.flatMap(f => [
+                    zpVals[f.key],
+                    tooltip,
+                    zpVals[f.key] !== null ? zpVals[f.key] - zpt_stdev : null,
+                    zpVals[f.key] !== null ? zpVals[f.key] + zpt_stdev : null
+                    //zpVals[f.key] !== null ? zpVals[f.key] + zpt_stdev : null, f.zp
+                ])
+            ];
+        });
+    
+        // Build table
+        const data = new google.visualization.DataTable();
+        data.addColumn('number', 'MJD');
+    
+        filters.forEach(f => {
+            const label = filterCounts[f.key] !== 0 ? filterCounts[f.key] : "N/A";
+            data.addColumn('number', `\${f.key} (\${label})`);
+            data.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
+            data.addColumn({type: 'number', role: 'interval'});
+            data.addColumn({type: 'number', role: 'interval'});
+            //data.addColumn('number');
+        });
+    
+        data.addRows(rows);
+    
+        const mjdMin1 = Math.min(...mjdVals);
+        const mjdMax1 = Math.max(...mjdVals);
+        const defaultSeries = { pointSize: 6, lineWidth: 0 };
+    
+        const titleText = `ZPs vs. MJD ({$proj}) â \${nRows} rows\${dataArray.length >= 5000 ? " (sampled)" : ""}`;
+
+        const options = {
+            title: titleText, 
+            titleTextStyle: { color: 'black', fontSize: 14 },
+            width: '100%',
+            height: Math.min(window.innerHeight * 0.6, 430),
+            legend: { position: 'top', alignment: 'center', maxLines: 3 },
+            //series: Object.fromEntries(filters.map((f, i) => [i*2, {
+            series: Object.fromEntries(filters.map((f, i) => [i, {
+                ...defaultSeries,
+                color: f.color,
+                pointShape: f.shape
+            }])),
+            intervals: { color: 'red', lineWidth: .0, style: 'sticks' },
+            hAxis: {
+                title: 'MJD - ' + mjdoff + ' (day)',
+                viewWindow: { min: 0, max: mjdMax1 + 0.005 },
+                gridlines: { count: -1 }
+            },
+            vAxis: {
+                title: 'Zeropoint (mag)',
+                viewWindow: { min: 22, max: 27 }
+            },
+            tooltip: { isHtml: true },
+            chartArea: { left: '9%', top: '10%', right: '3%', bottom: 50 },
+            fontSize: 14,
+        };
+	// options.series[1]  = { color: 'black', lineDashStyle: [2, 3], lineWidth: 2, pointSize: 0 };
+	// options.series[3]  = { color: 'black', lineDashStyle: [2, 3], lineWidth: 2, pointSize: 0 };
+	// options.series[5]  = { color: 'black', lineDashStyle: [2, 3], lineWidth: 2, pointSize: 0 };
+	// options.series[7]  = { color: 'black', lineDashStyle: [2, 3], lineWidth: 2, pointSize: 0 };
+	// options.series[9]  = { color: 'black', lineDashStyle: [2, 3], lineWidth: 2, pointSize: 0 };
+	// options.series[11] = { color: 'black', lineDashStyle: [2, 3], lineWidth: 2, pointSize: 0 };
+    
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_zpt'));
+        chart.draw(data, options);
+    }
+
+    // draw zoomed zerpoint vs mjd
+    function drawZOOMZPTChart() {
+        const mjdValues = dataArray.map(r => parseFloat(r.MJD));
+        let mjdMin0 = Math.min(...mjdValues);
+	const mjdMin = Math.floor(mjdMin0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        let mjdMax0 = Math.max(...mjdValues);
+	const mjdMax = Math.ceil(mjdMax0 * 10) / 10; // 1 decimal place or floor/ceil depending on your needs
+        // Extract the first MJD value from dataArray
+        const firstMJD = dataArray.length > 0 ? Math.floor(parseFloat(dataArray[0].MJD)) : "N/A";
+	const mjdoff = mjdMin;
+	// output at console
+        console.log("First MJD Value:", firstMJD);
+
+        // Compute median zpt_obs per filter
+        function median(arr) {
+            const sorted = arr.filter(v => !isNaN(v)).sort((a, b) => a - b);
+            const len = sorted.length;
+            if (len === 0) return null;
+            const mid = Math.floor(len / 2);
+            return len % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
+        }
+        
+        // Group zpt_obs by filter
+        const zptByFilter = { g: [], r: [], i: [], z: [], y: [], w: [] };
+        
+        dataArray.forEach(row => {
+            const f = row.filter[0]; // 'g','r','i','z','y','w'
+            const zpt = parseFloat(row.zpt_obs);
+            if (!isNaN(zpt) && zptByFilter.hasOwnProperty(f)) {
+                zptByFilter[f].push(zpt);
+            }
+        });
+        
+        // Compute medians (this replaces hardcoded zp0 values)
+        let gzp1 = median(zptByFilter.g);
+        let rzp1 = median(zptByFilter.r);
+        let izp1 = median(zptByFilter.i);
+        let zzp1 = median(zptByFilter.z);
+        let yzp1 = median(zptByFilter.y);
+        let wzp1 = median(zptByFilter.w);
+
+        let gzp0, rzp0, izp0, zzp0, yzp0, wzp0; // declare outside
+        switch ('{$proj}') {
+            case 'gpc1':
+              // ref?
+              gzp0 = 24.563; rzp0 = 24.750; izp0 = 24.611; zzp0 = 24.240; yzp0 = 23.320; wzp0 = 26.000;
+              // 
+              //gzp0 = 24.563; rzp0 = 24.750; izp0 = 24.611; zzp0 = 24.240; yzp0 = 23.320; wzp0 = 26.000;
+              break;
+            case 'gpc2':
+              // ref?
+              gzp0 = 25.013; rzp0 = 28.840; izp0 = 24.711; zzp0 = 24.300; yzp0 = 23.570; wzp0 = 26.000;
+              break;
+        }
+        
+        console.log("Dynamic median zeropoints per filter:", {
+            g: gzp0, r: rzp0, i: izp0, z: zzp0, y: yzp0, w: wzp0
+        });
+
+        // Build filters array (with safe label for missing zp)
+        const filters = [
+            { key: 'g', zp: gzp0, zpm: gzp1},
+            { key: 'r', zp: rzp0, zpm: rzp1},
+            { key: 'i', zp: izp0, zpm: izp1},
+            { key: 'z', zp: zzp0, zpm: zzp1},
+            { key: 'y', zp: yzp0, zpm: yzp1},
+            { key: 'w', zp: wzp0, zpm: wzp1}
+        ];
+
+        // Keep zpMap for quick lookup
+        const zpMap = {
+            g: gzp0, r: rzp0, i: izp0,
+            z: zzp0, y: yzp0, w: wzp0
+        };
+
+        const mjdVals = [];
+	let allZps = [];
+        const rows = dataArray.map(row => {
+            const mjd = parseFloat(row.MJD) - mjdoff; 
+            const zpt_obs = parseFloat(row.zpt_obs);    
+            const zpt_stdev = parseFloat(row.zpt_stdev);
+            const airmass = parseFloat(row.airmass);
+	    const filterChar = row.filter[0];      // 'g','r','i','z','y','w'
+            const filter = row.filter; 
+
+            // shift relative to median
+            let zpValues = { g: null, r: null, i: null, z: null, y: null, w: null };
+            if (zpMap.hasOwnProperty(filterChar) && !isNaN(zpt_obs) && zpMap[filterChar] !== null) {
+                zpValues[filterChar] = zpt_obs - zpMap[filterChar] ;
+            }
+
+            const tooltip  = buildTooltip(row);   // Tooltips
+
+	    // Build row for chart
+            const rowData = [mjd];
+            ["g", "r", "i", "z", "y", "w"].forEach(f => {
+                const zp = zpValues[f];
+                rowData.push( zp, tooltip, zp !== null ? zp - zpt_stdev : null, zp !== null ? zp + zpt_stdev : null);
+            });
+
+            // collect MJD offset values
+            mjdVals.push(mjd); 
+
+            // collect valid zp values
+	    Object.values(zpValues).forEach(v => {
+                if (v !== null && !isNaN(v)) allZps.push(v);
+            });
+
+            return rowData;
+        });
+
+	// find minimum zp across all filters
+        const minZp = Math.min(...allZps);
+	const clampedMinZp = minZp*2. < -0.5 ? -0.5 : minZp*2.;
+        const maxZp = Math.max(...allZps);
+	const clampedMaxZp = maxZp*2. >  0.5 ?  0.5 : maxZp*2.;
+        console.log("Min Zeropoint:", minZp, clampedMinZp);
+        console.log("Max Zeropoint:", maxZp, clampedMaxZp);
+	
+        const legendDiv = document.getElementById("legend");
+        legendDiv.innerHTML = "";  // clear before filling
+
+        // Define series styles (must match your chart series order)
+        const seriesStyles = [
+          { bgColor: '#ffffff', color: '#00f000', pointShape: 'circle' },   // g
+          { bgColor: '#ffffff', color: '#f00000', pointShape: 'triangle' }, // r
+          { bgColor: '#ffffff', color: '#f05000', pointShape: 'square' },   // i
+          { bgColor: '#ffffff', color: '#0050a0', pointShape: 'diamond' },  // z
+          { bgColor: '#ffffff', color: '#ffe000', pointShape: 'star' },     // y
+          { bgColor: '#ffffff', color: '#808080', pointShape: 'polygon' },  // w
+        ];
+        
+        // Map pointShape to simple Unicode / CSS symbol
+        const shapeSymbols = {
+          circle: 'â',
+          triangle: 'â²',
+          square: 'â ',
+          diamond: 'â',
+          star: 'â
+',
+          polygon: 'â¬'
+        };
+
+        // create data column
+        var data = new google.visualization.DataTable();
+        data.addColumn('number', 'MJD');
+        //filters.forEach(f => {
+        filters.forEach((f, index) => {
+
+          const zpLabel = f.zp !== null ? f.zp.toFixed(3) : "N/A";
+          const zpm     = f.zpm !== null ? f.zpm.toFixed(3) : "N/A";
+          data.addColumn('number', `\${f.key}:\${zpLabel}`);
+          // data.addColumn('number', `\${f.key}`);
+          data.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
+          data.addColumn({type: 'number', role: 'interval'});
+          data.addColumn({type: 'number', role: 'interval'});
+
+          // add to legend instead
+          const style = seriesStyles[index % seriesStyles.length];
+        
+          const span = document.createElement("span");
+          span.style.marginRight = "20px";
+          span.style.whiteSpace = "nowrap";
+          span.style.color = style.color;
+          span.style.backgroundColor = style.bgColor;
+        
+          // Use Unicode symbol for pointShape
+          const shape = shapeSymbols[style.pointShape] || 'â';
+        
+          span.innerText = `\${shape} \${f.key}:\${zpm}`;
+        
+          legendDiv.appendChild(span);
+
+        });
+
+        data.addRows(rows);
+
+	// plot options
+        const mjdMin1 = Math.min(...mjdVals);
+        const mjdMax1 = Math.max(...mjdVals);
+	const defaultSeries = { pointSize: 6, lineWidth: 0 };
+
+        const titleText = `ZPs_off vs. MJD ({$proj}) â \${nRows} rows\${dataArray.length >= 5000 ? " (sampled)" : ""}`;
+
+        const options = {
+            title: titleText, 
+            titleTextStyle: { color: 'black', fontSize: 15},
+            width: '100%',
+            height: Math.min(window.innerHeight * 0.6, 430), // This gives a nice result on smaller screens.
+            //legend: { position: 'top', alignment: 'center', maxLines: 3, textStyle: {fontSize: 12} },
+            legend: { position: 'none', alignment: 'center', maxLines: 3 },
+            series: {
+                0: { ...defaultSeries, color: '#00f000', pointShape: 'circle',   },  // 'g' filter
+                1: { ...defaultSeries, color: '#f00000', pointShape: 'triangle', },  // 'r' filter
+                2: { ...defaultSeries, color: '#f05000', pointShape: 'square',   },  // 'i' filter
+                3: { ...defaultSeries, color: '#0050a0', pointShape: 'diamond',  },  // 'z' filter
+                4: { ...defaultSeries, color: '#ffe000', pointShape: 'star',     },  // 'y' filter
+                5: { ...defaultSeries, color: '#808080', pointShape: 'polygon',  },  // 'w' filter
+            },
+            intervals: { color: 'red', lineWidth: .0, style: 'sticks' },
+            hAxis: {
+                title: 'MJD - ' + mjdoff + ' (day)',
+		viewWindow: {
+	           min: 0,  // small padding
+                   max: mjdMax1 + 0.005
+                },
+	        gridlines: { count: -1 },
+                minorGridlines: { count: 1 }
+            },
+            vAxis: {
+                title: 'Zeropoint offset (mag)',
+                //viewWindow: {   min:  clampedMinZp, max: clampedMaxZp },
+                viewWindow: {   min:  -0.3, max: 0.15 },
+	        gridlines: { count: -1 },
+                minorGridlines: { count: 1 }
+            },
+	    tooltip: { isHtml: true},
+            chartArea: { left: '9%', top: '12%', right: '3%', bottom: 50 },
+            fontSize: 14,
+        };
+
+        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_zoomzpt'));
+        chart.draw(data, options);
+
+    }
+    google.charts.setOnLoadCallback(drawCHIPChart);
+    google.charts.setOnLoadCallback(drawFWHMChart);
+    google.charts.setOnLoadCallback(drawZPTChart);
+    google.charts.setOnLoadCallback(drawZOOMZPTChart);
+
+</script>
+
+<div class="chartWithOverlay" style="position: relative; width: 100%; height:440px;">
+    <div id="dq_div_chips" style="width:100%; border:1px solid #000;"></div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 100%; height:440px;">
+    <div id="dq_div_zpt" style="width:100%; border:1px solid #000;"></div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 100%; height:440px;">
+    <div id="dq_div_zoomzpt" style="width:100%; border:1px solid #000;"></div>
+    <div id="legend" class="overlay"
+         style="position:absolute; top:50px; left:55%; transform:translateX(-50%); 
+                font-size:15px; white-space: nowrap; display:flex; align-items:center; "> </div>
+</div>
+
+<div class="chartWithOverlay" style="position: relative; width: 100%; height:440px;">
+    <div id="dq_div_fwhm" style="width:100%; border:1px solid #000;"></div>
+</div>
+
+
+HTML;
+    // End timer
+    $total_time = round(microtime(true) - $start, 3);
+    echo "<center>loading plot in $total_time seconds<br></center>";
+
+}
+
+?>
+
Index: /trunk/ippMonitor/raw/pointing.php
===================================================================
--- /trunk/ippMonitor/raw/pointing.php	(revision 42971)
+++ /trunk/ippMonitor/raw/pointing.php	(revision 42972)
@@ -15,5 +15,5 @@
 else {$myMenu = "ipp.czar.dat";}
 
-menu($myMenu, 'Czartool on '.$lastUpdateTime, 'ipp.css', $ID['link'], $ID['proj']);
+menu($myMenu, 'Czartool on ', 'ipp.css', $ID['link'], $ID['proj']);
 
 $pass = $ID['pass'];
@@ -22,483 +22,55 @@
 
 // Fetch data based on range selection
-$range = isset($_GET['range']) ? $_GET['range'] : 'yday';
+$range = isset($_GET['range']) ? $_GET['range'] : 'week';
+
+require_once 'functions.php';
 
 echo "<table border=\"0\">";
 echo "<tr>";
 echo "  <td style=width:1240>";
-echo "    <h1 align=\"middle\">Pointing Report</h1>";
-echo "  <center>";
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=yday">Last night (default)</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=day">Today (MJD '.getMJD().')</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=week">Past 7 days</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=month">Past 30 days</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=year">Past 365 days</a> | ';
-echo '    <a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=all">All </a>   ';
+echo "    <h1 align=\"middle\">Data Quality Report</h1>";
+
+#
+# project selector
+echo '<center>';
+# Project selector with highlight
+$projects = ["gpc1" => "GPC1", "gpc2" => "GPC2"];
+foreach ($projects as $key => $label) {
+    if ($proj == $key) {
+        echo "<strong style='color:blue;'>$label</strong> | ";
+    } else {
+        echo '<a href="?pass=' . urlencode($pass) . '&proj=' . $key . '&range=' . urlencode($range) . '">' . $label . '</a> | ';
+    }
+}
+echo "<br>";
+
+# range links with highlighting
+$ranges = [
+    "today"     => "Today",
+    "yesterday" => "Yesterday",
+    "week"      => "Week",
+    "month"     => "Month",
+    "year"      => "Year",
+    "all"       => "All"
+];
+
+foreach ($ranges as $key => $label) {
+    if ($range == $key) {
+        echo "<strong style='color:blue;'>$label</strong> | ";
+    } else {
+        echo '<a href="?pass=' . urlencode($pass) . '&proj=' . urlencode($proj) . '&range=' . $key . '">' . $label . '</a> | ';
+    }
+}
+
 echo "  </td>";
 echo "</tr>";
 echo "</table>";
-echo getPointing($projectdb, $range);
 
+echo "    <div style=\"margin-bottom:10px; font-size:14px; color:#555;\">";
+echo "      Current MJD: " . getMJD();
+echo "    </div>";
+
+echo getPointing($projectdb, $range, $proj);
 menu_end();
-
-
-/////////////////////////////////////////////////////////////////////////////
-//                                                                         //
-//                               Functions                                 //
-//                                                                         //
-/////////////////////////////////////////////////////////////////////////////
-###########################################################################
-#
-# Get data quality
-#
-###########################################################################
-function getPointing($db, $range = 'yday') {
-    // Start timer
-    $start = microtime(true);
-
-    $mjdDay  = getMJD();
-    $mjdDay_yearback = $mjdDay - 366;
-    $mjdDay_monthback = $mjdDay - 32;
-    $min_exp_id = 0;
-
-    // Determine the reference exp_name based on the selected range
-    switch ($range) {
-        case 'year':
-            $expname = "o" . $mjdDay_yearback . "%";
-            break;
-        case 'month':
-            $expname = "o" . $mjdDay_monthback . "%";
-            break;
-        case 'week':
-            $expname = "o" . ($mjdDay - 7) . "%";
-            break;
-        case 'day':
-            $expname = "o" . $mjdDay . "%";
-            break;
-        default:
-            $expname = "o" . ($mjdDay - 1) . "%"; // Default: last night
-            break;
-    }
-
-    // Query to get the minimum exp_id
-    $expIdQuery = "SELECT MIN(exp_id) AS min_exp_id FROM rawExp WHERE rawExp.exp_name LIKE '$expname'";
-    $result = $db->query($expIdQuery);
-
-    if (DB::isError($result)) {
-        die("Database error: " . $result->getMessage());
-    }
-
-    $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
-
-    if (!empty($row['min_exp_id'])) {
-        $min_exp_id = $row['min_exp_id'];
-    //    echo "Minimum exp_id: " . $min_exp_id;
-    } else {
-        echo "No results found.";
-    }
-
-    //echo "<br><br>";
-
-    // Determine the date constraint based on the range
-    switch ($range) {
-        case 'year':
-            $dateConstraint = "dateobs >= NOW() - INTERVAL 365 DAY AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'month':
-            $dateConstraint = "dateobs >= NOW() - INTERVAL 30 DAY AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'week':
-            $expnames = array();
-            for ($i = 0; $i < 7; $i++) {
-                $expnames[] = "rawExp.exp_name LIKE 'o" . ($mjdDay - $i) . "%'";
-            }
-            $dateConstraint = "(" . implode(" OR ", $expnames) . ") AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'yday':
-            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'day':
-            $dateConstraint = "rawExp.exp_name LIKE 'o$mjdDay%' AND rawExp.exp_id >= $min_exp_id";
-            break;
-        case 'all':
-            $dateConstraint = "rawExp.exp_id >= 0";
-            break;
-        default: // Default to last night
-            $dateConstraint = "rawExp.exp_name LIKE 'o" . ($mjdDay - 1) . "%' AND rawExp.exp_id >= $min_exp_id";
-            break;
-    }
- 
-    // Updated SQL query
-    $sql = "select * from (SELECT SUBTIME(dateobs, '10:00:00') AS time, AST_R0, AST_D0,
-                   round(( 367*YEAR(dateobs)- FLOOR((7 * (YEAR(dateobs) + FLOOR((MONTH(dateobs) + 9) / 12))) / 4) + FLOOR(275 * MONTH(dateobs) / 9)
-                   + DAY(dateobs) + 1721013.5 + (HOUR(dateobs) / 24.0) + (MINUTE(dateobs) / 1440.0) + (SECOND(dateobs) / 86400.0) - 2400000.5), 5) AS MJD,
-                   filter, exp_time, comment, ra*180/pi() as RA, decl*180/pi() as DECL, AST_RS, AST_DS, AST_T0, posang, exp_name, alt, az, rawExp.exp_id, camRun.label
-            FROM rawExp
-            JOIN chipRun USING (exp_id)
-            JOIN camRun USING (chip_id)
-            JOIN camProcessedExp USING (cam_id)
-            WHERE $dateConstraint
-              AND exp_type = 'OBJECT'
-              AND camRun.state LIKE 'full'
-              AND camProcessedExp.fault = 0
-              AND camProcessedExp.zpt_obs != 0 ORDER BY RAND() limit 5000) as sub order by time;";
-
-    $DQarray = array();
-    $qry = $db->query($sql);
-    if (dberror($qry)) {
-        echo "<b>Error with $sql</b><br>\n";
-        return;
-    }
-    //    echo "$sql<br><br>\n";
-    
-    while ($qry->fetchInto($row)) {
-        $DQarray[] = $row;
-    }
-    
-    // Ensure $DQarray is not empty
-    if (empty($DQarray)) {
-        echo "<center><b> No data for MJD: $mjdDay </b></center>";
-        return; // Stop execution if no data to encode
-    }
-    
-    // Convert data array to JSON for easier handling in JavaScript
-    $dataJson = json_encode($DQarray);
-    
-    if ($dataJson === false) {
-        echo "<b>Error encoding JSON:</b> " . json_last_error_msg(); // Display any JSON encoding errors
-        return; // Stop execution if JSON encoding fails
-    }
-    
-    echo "<script>console.log(" . json_encode($dataJson) . ");</script>"; // Log JSON output to console for debugging
-echo <<<HTML
-<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js" onerror="loadLocalGoogleCharts()"></script>
-
-<script type="text/javascript">
-    // Check if Google Charts library loaded successfully
-    window.addEventListener("load", function() {
-        if (typeof google === "undefined" || typeof google.charts === "undefined") {
-            loadLocalGoogleCharts();
-        }
-    });
-
-    // Function to load local version of Google Charts if CDN fails
-    function loadLocalGoogleCharts() {
-        console.warn("Google Charts CDN failed, loading local loader.js instead.");
-        var script = document.createElement("script");
-        script.src = "loader.js"; // Path to your local loader.js file
-        document.head.appendChild(script);
-    }
-</script>
-
-<script type="text/javascript">
-    google.charts.load('current', {packages: ['corechart']});
-
-    // Use the encoded JSON directly in JavaScript
-    const dataArray = JSON.parse('{$dataJson}'); // Directly parse the PHP JSON string
-
-    // draw pointing vs mjd
-    function drawMJDChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'MJD');
-            data.addColumn('number', 'RA_offset (AST_R0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'Dec_offset (AST_D0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', '20 TP unit ~5.1"');
-            data.addColumn('number', '-20 TP unit ~5.1"');
-
-        const rows = dataArray.map(row => {
-            const [timestamp, ra_offset, dec_offset, mjd, filter, exp_time, comment, ra, dec, ra_offset_sig, dec_offset_sig, boresite_ang, position_ang, exp_name, alt, az, exp_id, cam_label] = row;
-
-            return [
-                parseFloat(mjd),               // MJD
-                parseFloat(ra_offset),         // RA_offset (AST_R0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                parseFloat(dec_offset),        // Dec_offset (AST_D0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                20,                            // Dummy value for 20 TP unit
-                -20                            // Dummy value for -20 TP unit
-            ];
-        });
-
-        data.addRows(rows);
-
-        // Extract the first MJD value from dataArray
-        //const firstMJD = dataArray.length > 0 ? parseFloat(dataArray[0][1]) : "N/A";
-        const firstMJD = dataArray.length > 0 ? Math.floor(parseFloat(dataArray[0][3])) : "N/A";
-        console.log("First MJD Value:", firstMJD);
-
-        const options = {
-            title: 'Pointing Offset (MJD: '+firstMJD+')',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
-                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
-                2: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }, // Line at y = 20
-                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }  // Line at y = -20
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'Offset (TP units)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
-            fontSize: 15,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_mjd'));
-        chart.draw(data, options);
-    }
-
-    // draw pointing scatter
-    function drawSIGChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'MJD');
-            data.addColumn('number', 'RA_offset_scatter (AST_RS)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'Dec_offset_scatter (AST_DS)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-
-        const rows = dataArray.map(row => {
-            const [timestamp, ra_offset, dec_offset, mjd, filter, exp_time, comment, ra, dec, ra_offset_sig, dec_offset_sig, boresite_ang, position_ang, exp_name, alt, az, exp_id, cam_label] = row;
-
-            return [
-                parseFloat(mjd),               // MJD
-                parseFloat(ra_offset_sig),     // RA_offset_scatter (AST_RS)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                parseFloat(dec_offset_sig),    // Dec_offset_scatter (AST_DS)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'Pointing Offset Scatter (MJD)',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
-                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
-                2: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }, // Line at y = 20
-                3: { color: 'red', lineWidth: 2, lineDashStyle: [8, 4], pointSize: 0 }  // Line at y = -20
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'MJD',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'Offset Scatter (TP units)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
-            fontSize: 15,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_sig'));
-        chart.draw(data, options);
-    }
-
-
-    // position angle
-    function drawPOSChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'MJD');
-            data.addColumn('number', 'RA_offset (AST_R0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'Dec_offset (AST_D0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-
-        const rows = dataArray.map(row => {
-            const [timestamp, ra_offset, dec_offset, mjd, filter, exp_time, comment, ra, dec, ra_offset_sig, dec_offset_sig, boresite_ang, position_ang, exp_name, alt, az, exp_id, cam_label] = row;
-
-            return [
-                parseFloat(position_ang),      // Position Angle
-                parseFloat(ra_offset),         // RA_offset (AST_R0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                parseFloat(dec_offset),        // Dec_offset (AST_D0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'Pointing Offset (Position Angle)',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            // pointSize: 5, // Point size for scatter plot
-            // lineWidth: 2,
-            series: {
-                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
-                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'Position Angle',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'Offset (TP units)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
-            fontSize: 15,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_pos'));
-        chart.draw(data, options);
-    }
-
-    function drawALTChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'Altitute');
-            data.addColumn('number', 'RA_offset (AST_R0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'Dec_offset (AST_D0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-
-        const rows = dataArray.map(row => {
-            const [timestamp, ra_offset, dec_offset, mjd, filter, exp_time, comment, ra, dec, ra_offset_sig, dec_offset_sig, boresite_ang, position_ang, exp_name, alt, az, exp_id, cam_label] = row;
-
-            return [
-                parseFloat(alt),      // Position Angle
-                parseFloat(ra_offset),         // RA_offset (AST_R0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                parseFloat(dec_offset),        // Dec_offset (AST_D0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'Pointing Offset (Altitude)',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
-                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'Altitude',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'Offset (TP units)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            //chartArea: {left: 80, top: 50, right: 20, bottom: 50},
-            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
-            fontSize: 15,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_alt'));
-        chart.draw(data, options);
-    }
-
-   function drawAZChart() {
-        var data = new google.visualization.DataTable();
-            data.addColumn('number', 'Azumith');
-            data.addColumn('number', 'RA_offset (AST_R0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-            data.addColumn('number', 'Dec_offset (AST_D0)');
-            data.addColumn({'type': 'string', 'role': 'tooltip', 'p': {'html': true}});
-
-        const rows = dataArray.map(row => {
-            const [timestamp, ra_offset, dec_offset, mjd, filter, exp_time, comment, ra, dec, ra_offset_sig, dec_offset_sig, boresite_ang, position_ang, exp_name, alt, az, exp_id, cam_label] = row;
-
-            return [
-                parseFloat(az),      // Position Angle
-                parseFloat(ra_offset),         // RA_offset (AST_R0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-                parseFloat(dec_offset),        // Dec_offset (AST_D0)
-                "expname: "+exp_name+"<br>exp_id: "+exp_id+"<br>timestamp: "+timestamp+"<br>RA: "+ra+"<br>DEC: "+dec+"<br>filter: "+filter+"<br>comment: "+comment+"<br>cam_label: "+cam_label,
-            ];
-        });
-
-        data.addRows(rows);
-
-        const options = {
-            title: 'Pointing Offset (Azumith)',
-            titleTextStyle: {color: 'black', fontSize: 15},
-            width: '100%',
-            height: 480,
-            legend: { position: 'top', alignment: 'center' },
-            series: {
-                0: { color: '#0000ff', pointSize: 3, lineWidth: 0 }, // RA_offset series
-                1: { color: '#33a532', pointSize: 3, lineWidth: 0 }, // Dec_offset series
-            },
-            intervals: { color: 'red', lineWidth: 2, style: 'sticks' },
-            hAxis: {
-                title: 'Azumith',
-                gridlines: {count: -1}
-            },
-            vAxis: {
-                title: 'Offset (TP units)',
-            },
-            tooltip: { isHtml: true }, // Render tooltips as HTML
-            //chartArea: {left: 80, top: 50, right: 20, bottom: 50},
-            chartArea: {left: '7%', top: 60, right: '4%', bottom: 50}, // Adjust left and right
-            fontSize: 15,
-        };
-
-        const chart = new google.visualization.ScatterChart(document.getElementById('dq_div_az'));
-        chart.draw(data, options);
-    }
-
-    google.charts.setOnLoadCallback(drawMJDChart);
-    google.charts.setOnLoadCallback(drawSIGChart);
-    google.charts.setOnLoadCallback(drawPOSChart);
-    google.charts.setOnLoadCallback(drawALTChart);
-    google.charts.setOnLoadCallback(drawAZChart);
-</script>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_mjd" style="width:1240px;"></div>
-    <div class="overlay" style="position: absolute; width: 800px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_sig" style="width:1240px;"></div>
-    <div class="overlay" style="position: absolute; width: 800px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_pos" style="width:1240px;"></div>
-    <div class="overlay" style="position: absolute; width: 800px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_alt" style="width:1240px;"></div>
-    <div class="overlay" style="position: absolute; width: 800px; bottom: 60px; left: 85px;"></div>
-</div>
-
-<div class="chartWithOverlay" style="position: relative; width: 1240px; height:500px;">
-    <div id="dq_div_az" style="width:1240px;"></div>
-    <div class="overlay" style="position: absolute; width: 800px; bottom: 60px; left: 85px;"></div>
-</div>
-HTML;
-
-// End timer
-$total_time = round(microtime(true) - $start, 3);
-echo "<center>Loading plot in $total_time seconds<br></center>";
-
-}
 
 ?>
Index: /trunk/ippMonitor/raw/storage.php
===================================================================
--- /trunk/ippMonitor/raw/storage.php	(revision 42971)
+++ /trunk/ippMonitor/raw/storage.php	(revision 42972)
@@ -12,8 +12,9 @@
 $projectdb = dbconnect($ID['proj']);
 
+
 if ($ID['menu']) {$myMenu = $ID['menu'];}
 else {$myMenu = "ipp.czar.dat";}
 
-menu($myMenu, 'Czartool on '.$lastUpdateTime, 'ipp.css', $ID['link'], $ID['proj']);
+menu($myMenu, 'Czartool on ', 'ipp.css', $ID['link'], $ID['proj']);
 
 $pass = $ID['pass'];
@@ -25,4 +26,7 @@
 echo "  <td style=width:1200px>";
 echo "  <h1 align=\"middle\">Storage status</h1>";
+
+require_once 'functions.php';  
+
 # storage timeseries
 echo getSpaces($czardb);
@@ -35,277 +39,4 @@
 menu_end();
 
-
-/////////////////////////////////////////////////////////////////////////////
-//                                                                         //
-//                               Functions                                 //
-//                                                                         //
-/////////////////////////////////////////////////////////////////////////////
-
-###########################################################################
-#
-# Gets disk status
-#
-###########################################################################
-function getHosts($db) {
-    $start = microtime(true);
-
-
-    // Set environment variables
-    putenv("PATH=/usr/local/bin:/usr/bin:/bin:/data/ippc65.0/ipp/src/psconfig//ipp-20210708-gentoo.lin64/bin");
-    putenv("PERL5LIB=/data/ippc65.0/ipp/src/psconfig//ipp-20210708-gentoo.lin64/lib");
-
-    $hostnoteall = array();
-    exec("neb-host |cut -b 17-30,86-", $hostnoteall);
-
-    $sql = "SELECT host, format(total, 2), format(available, 2), format(used, 2), writable, readable, format(used/total*100,3) as ratio FROM hosts where xattr <> 3 ORDER BY CASE WHEN ROUND(used/total*100,3) = 100 THEN 0 ELSE 1 END, xattr, host;";
-    $qry = $db->query($sql);
-
-    if ($qry === false) {
-        echo "<b>Error with SQL query</b><br>";
-        return;
-    }
-
-    $sqlMax = "SELECT MAX(total) AS max_total FROM hosts WHERE xattr <> 3";
-    $qryMax = $db->query($sqlMax);
-    $maxUsed = 0;
-    
-    if ($qryMax && $qryMax->fetchInto($rowMax)) {
-        $maxUsed = $rowMax[0];  // max_total value
-    }
-
-    $dataRows = array();
-    while ($qry->fetchInto($row)) {
-        // Access by index
-        $host      = $row[0]; // ipp071.0
-        $total     = $row[1]; // 80.0352 TB
-        $available = $row[2]; // 10.8096 TB
-        $used      = $row[3]; // 69.2256 TB
-        $writable  = $row[4]; // 0
-        $readable  = $row[5]; // 1
-        $ratio     = $row[6]; // 86.4939 %
-
-        // Match notes
-        $hostnotes = preg_grep("/$host/", $hostnoteall);
-        $hostnote = reset($hostnotes);
-
-        // Defaults
-        $styles    = "stroke-width: .5; stroke-color: black;";
-        $status    = "down";
-        $usedColor = "lightgrey";
-        $freeColor = "lightgrey";
-
-        // Status logic
-        if ($writable == 1 && $readable == 1) {
-            $status    = "up";
-            $usedColor = ($ratio >= 97) ? "pink" : "yellow"; // red if â¥97%
-            $freeColor = "#a6ec99"; // green free
-        } elseif ($writable == 0 && $readable == 1) {
-            $status    = "repair";
-            $usedColor = ($ratio >= 97) ? "pink" : "yellow";
-            $freeColor = "lightgrey"; // grey free
-        } elseif ($available < 1) {
-            $used      = $maxUsed;
-            $available = 0;
-            $status    = "offline";
-            $usedColor = "white";
-            $freeColor = "white";
-        }
-    
-        if ($status == "down") {
-            $tooltipUsed = $host . ": " . round($ratio, 1) . "% of " . $total . " TB used ". 
-                           "<span style='color:red; font-weight:bold;'> <br>" . $hostnote .  "</span>";
-            $tooltipFree = "<span style='color:red; font-weight:bold;'>" .
-                           $host . ": " . $available . " TB free" .
-                           "</span>";
-        } elseif ($status == "offline") {
-            $tooltipUsed = $host . ": " . round($ratio, 1) . "% of " . $total . " TB used ". 
-                           "<span style='color:red; font-weight:bold;'> <br>" . $hostnote .  "</span>";
-            $tooltipFree = "<span style='color:red; font-weight:bold;'>" .
-                           $host . ": " . $total . " TB free" .
-                           "</span>";
-        } else {
-            $tooltipUsed = $host . ": " . round($ratio, 1) . "% of " . $total . " TB used<br>" . $hostnote ;
-            $tooltipFree = $host . ": " . $available . " TB free";
-        }
-
-
-        // Build data row (manual string concatenation for old PHP)
-        $dataRows[] =
-            "[ \"" . $host . ":" . $status . "\", " .
-            $used . ", \"" . $styles . " color: " . $usedColor . ";\", \"" . $tooltipUsed . "\", " .
-            $available . ", \"" . $styles . " color: " . $freeColor . ";\", \"" . $tooltipFree . "\" ]";
-
-    }
-    echo "<script type=\"text/javascript\">
-            google.charts.load('current', {packages: ['corechart', 'bar']});
-            google.charts.setOnLoadCallback(drawBarColors);
-
-            function drawBarColors() {
-                var data = google.visualization.arrayToDataTable([
-                    ['Host', 'Used', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}}, 
-                     'Free', {type: 'string', role: 'style'}, {type: 'string', role: 'tooltip', 'p': {'html': true}}],
-                    " . implode(",\n", $dataRows) . "
-                ]);
-                
-                var options = {
-                    title: 'Nebulous Disk Use across IPP Clusters',
-                    titleTextStyle: {color: 'black', fontSize: 15},
-                    width: '100%',
-                    height: '100%',
-                    tooltip: {isHtml: true},
-                    legend: { position: 'none', alignment: 'end', maxLines: 3 },
-                    hAxis: { title: 'Space (TB)', gridlines: {count: 20} },
-                    vAxis: { textStyle: {fontSize: 14}, format: 'string',  textPosition: 'in', direction: 1},
-                    bar: { groupWidth: '95%' },
-                    chartArea: {left:40, top:50, right:20, bottom:40},
-                    fontSize: 15,
-                    allowHtml: true,
-                    isStacked: true
-                };
-
-                var chart = new google.visualization.BarChart(document.getElementById('disk_div'));
-                chart.draw(data, options);
-            }
-          </script>";
-
-echo "<script type='text/javascript' src='loader.js'></script>
-<br>
-<div class='chartWithOverlay' style='position: relative; width: 100%'>
-
-    <div id='disk_div' style='width:100%; height:5000px'></div>
-
-    <div class='overlay' style='position: absolute; width:160px; top:50px; right:30px; font-family:Arial, sans-serif;'>
-
-      <div style='font-size:15px; border:1px solid #888; background-color:lightgreen; color:black; text-align:center; margin-bottom:1px;'>Free</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:yellow    ; color:black; text-align:center; margin-bottom:1px;'>Used</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:pink      ; color:black; text-align:center; margin-bottom:1px;'>97% Usage</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:lightgrey ; color:black; text-align:center; margin-bottom:1px;'>Down/Repair</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:white     ; color:black; text-align:center; margin-bottom:1px;'>Offline</div>
-      <div style='font-size:15px; border:1px solid #888; background-color:#42a5f5   ; color:white; text-align:center; margin-bottom:1px;'>
-        <a href='https://atrcganglia.ifa.hawaii.edu/' target='_blank' style='text-decoration:none; color:white; font-weight:bold;'>ATRC ganglia</a>
-      </div>
-
-    </div>
-
-</div>";
-
-    $total_time = round(microtime(true) - $start, 3);
-    echo "<center>loading in $total_time seconds<br></center>";
-}
-
-
-###########################################################################
-#
-# Gets disk spaces status
-#
-###########################################################################
-function getSpaces($db) {
-    // timer start
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $start = $time;
-
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ippb%' and xattr <> 3")->fetchInto($row);
-    $ippbusable = $row[0];
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ippb%' and writable = 1 and xattr <> 3")->fetchInto($row);
-    $ippbfree = $row[0];
-
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host not like 'ippb%' and xattr <> 3")->fetchInto($row);
-    $ippusable = $row[0];
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host not like 'ippb%' and writable = 1 and xattr <> 3")->fetchInto($row);
-    $ippfree = $row[0];
-
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ipp%_bck.0' and xattr <> 3")->fetchInto($row);
-    $bckusable = $row[0];
-    $db->query("SELECT FORMAT(IFNULL(SUM(available), 0), 1) FROM hosts where host like 'ipp%_bck.0' and writable = 1 and xattr <> 3")->fetchInto($row);
-    $bckfree = $row[0];
-
-
-    $sql = "select format(used/total*100,1), usable from cluster_space order by timestamp desc limit 1;";
-
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    $qry->fetchInto($row);
-    $allocated = $row[0];
-    $usable    = $row[1];
-
-    #echo "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>";
-    echo "<script type=\"text/javascript\" src=\"loader.js\"></script>";
-    echo "<br><div class=\"chartWithOverlay\" style=\"position: relative; width: 100%; height:400\">"; 
-    echo "  <div id=\"space_div\" style=\"width:100%; \"></div>";
-    echo "  <div class=\"overlay\" style=\"position: absolute; width: 500px; bottom: 60px; left: 80px;\">";
-    echo "    <div style=\"font-size: 16px; color:#0000ff; \"><b>ipp*_bck: $bckusable TB (usable)</b></div>";
-    echo "    <div style=\"font-size: 16px; color:#33a532; \"><b>ipp*_bck: $bckfree   TB (free)</b></div>";
-    echo "  </div>";
-    echo "</div>";
-
-    echo "<script type=\"text/javascript\">";
-    echo "google.charts.load('current', {packages: ['corechart']});";
-    echo "google.charts.setOnLoadCallback(drawChart);";
-    echo "function drawChart() {";
-    echo "  var data = new google.visualization.DataTable();";
-    echo "        data.addColumn('datetime', 'Day');";
-    echo "        data.addColumn('number', 'Usable (ipp:$ippusable, ippb:$ippbusable) TB');";
-    echo "        data.addColumn('number', 'Free (ipp:$ippfree, ippb:$ippbfree) TB');";
-    echo "   data.addRows([";
-    $interval = 10;
-    $sql = "select sub.dated, sub.usable, sub.available from (select date_format(timestamp, \"%Y.%m.%d.%H.%i\") as dated, usable, available from cluster_space where timestamp > curdate() - INTERVAL $interval DAY) as sub group by dated;";
-    if ($debug) {echo "$sql<br>";}
-    $qry = $db->query($sql);
-    if (dberror($qry)) {echo "<b>error with $sql </b><br>\n";}
-    while ($qry->fetchInto($row)) {
-        $tsp = explode('.', $row[0]);
-	$mon = $tsp[1]-1;
-        $timestamp = $row[0];
-        $usable    = $row[1];
-        $available = $row[2];
-#	new Date(Year, Month, Day, Hours, Minutes, Seconds, Milliseconds)
-       echo "[new Date($tsp[0], $mon, $tsp[2], $tsp[3], $tsp[4]), $usable, $available],";
-    }
-   echo "  ]);";
-   echo "   var options = {";
-   echo "      chart: {";  
-   echo "               position: 'center', " ;
-   echo "              },";
-   echo "     title: 'Total Available Cluster Space in the Past $interval Days ($allocated% of total allocated)',";
-   echo "     titleTextStyle: {color: 'black', fontSize: 15},";
-   echo "     width: '100%',";
-   echo "     height: 400,";
-   echo "     legend: { position: 'top',";
-   echo "               alignment: 'center',";
-   echo "              },";
-   echo "     lineWidth: 2, ";
-   echo "     series: {";
-   echo "         0: { color: '#0000ff', lineWidth: 3 },      ";
-   echo "         1: { color: '#33a532', lineWidth: 3 },      ";
-   echo "              },";
-   echo "     hAxis: {  ";
-   echo "             title: 'Date/Time (HST)',";
-   echo "             format: 'MMM dd',";
-   echo "            gridlines: {count: -1},"; 
-   echo "     },";
-   echo "     vAxis: {  ";
-   echo "                  title: 'Available (TB)',";
-   echo "     viewWindow: {   min: 500    },  ";
-   echo "     },";
-   echo "     chartArea: {left:80, top:50, right:20, bottom:50},";
-   echo "     fontSize: 12,";
-   echo "   };";
-   echo "   var chart = new google.visualization.LineChart(document.getElementById('space_div'));";
-   echo "   chart.draw(data, options);";
-   echo " }";
-   echo "</script>";
-    $time = microtime();
-    $time = explode(' ', $time);
-    $time = $time[1] + $time[0];
-    $finish = $time;
-    $total_time = round(($finish - $start), 3);
-    $start= $finish;
-    echo "<center>loading in $total_time seconds<br></center>";
-}
-
-
 ?>
 
