Initial commit

Add spruce scraper with CLI, session management, parsers, progress tracking,
recheck logic, and test suite. Includes example config and README.
This commit is contained in:
2026-04-22 10:41:18 -04:00
commit e122f6435a
23 changed files with 3789 additions and 0 deletions
View File
+17
View File
@@ -0,0 +1,17 @@
"""Shared pytest fixtures."""
from pathlib import Path
import pytest
FIXTURES = Path(__file__).parent / "fixtures"
@pytest.fixture
def scan_list_html() -> str:
return (FIXTURES / "scan_list.html").read_text(encoding="utf-8")
@pytest.fixture
def scan_view_html() -> str:
return (FIXTURES / "scan_view.html").read_text(encoding="utf-8")
+782
View File
@@ -0,0 +1,782 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Root View</title>
<!-- <script src="menuscript.js" language="javascript" type="text/javascript"></script> -->
<link rel="stylesheet" type="text/css" href="menustyle.css" media="screen, print" />
<script src="menuscript.js" language="javascript" type="text/javascript"></script>
</head>
<body>
<style>
P {
font: 9pt Arial, Helvetica, sans-serif;
}
.header {
font: bold 22pt Arial, Helvetica, sans-serif;
color: #008080;
text-align: center;
}
.header_desc {
font: bold 10pt Arial, Helvetica, sans-serif;
color: #800000;
text-align: center;
}
.error {
font: bold 16pt Arial, Helvetica, sans-serif;
color: #ff0000;
text-align: center;
}
.information {
font: 10pt Arial, Helvetica, sans-serif;
color: #0000ff;
text-align: left;
}
.data_title {
font: bold 9pt Arial, Helvetica, sans-serif;
color: white;
background: #696969;
text-align: left;
}
.data_odd {
font: 9pt Arial, Helvetica, sans-serif;
font-weight : lighter;
background-color : #eeeeee;
color : #000000;
}
.data_even {
font: 9pt Arial, Helvetica, sans-serif;
font-weight : lighter;
background-color : #ffffff;
color : #000000;
}
.data {
font: 9pt Arial, Helvetica, sans-serif;
color: #000000;
}
.login {
font: bold 9pt Arial, Helvetica, sans-serif;
}
.header_link {
font: bold 9pt Arial, Helvetica, sans-serif;
color: white;
background: #696969;
text-align: left;
text-decoration: none;
}
#pointer_div {
position:relative;
border-style:solid;
border-width:2px;
border-color:red;
margin:5px;
/*cursor:crosshair;*/
}
#data_left {
text-align: left;
}
#image_frame {
background-color: #70a0b0;
font-size:2px;
}
.red_text {
color: #ff0000;
font: bold 9pt Arial, Helvetica, sans-serif;
}
</style>
<script>
function validateFilename(filenameID) {
var item = getObj(filenameID);
var result = item.value;
// based on code found on http://www.codingforums.com/showthread.php?t=194468
if (/^[a-z0-9\.\s\-\_\[\]]*$/i.test(result) === false) { // anything but a-zA-Z0-9, [,], dot, hypen, space, underscore is disallowed
alert ("File name " + result + " contains invalid character(s)! \r\nAnything but a-z, A-Z, 0-9, square brackets, dot, hypen, space, underscore is disallowed");
item.focus();
return false;
}
// If we allow using spaces in filenames, we should at least strip any leading or trailing spaces
item.value = item.value.replace(/^\s+|\s+$/g,""); // the g switch is essential!!
return true;
}
function getObj(ElementId)
{
if (document.getElementById) // BYI now this method works perfect for both IE and NS
return document.getElementById(ElementId);
return false;
}
function showit(ElementId)
{
getObj(ElementId).style.display='block';
}
function hideit(ElementId)
{
getObj(ElementId).style.display='none';
}
function FormatFloat(v)
{
if (v=="")
return v;
return Math.round(v*100)/100;
}
function validateNumber(objId){
// BYI the function validates if the value of the data entry element on the form is an integer or a float
// an alert message is popped up in case of error, the problem field is focused
// the function returns false so that if used in OnSubmit clause it will force the user to re-enter the data
var item = getObj(objId);
var result = item.value;
if (isNaN(Number(result))) {
alert("The value must be an integer or float value.");
item.focus();
return false;
}
return true;
}
function validatePositiveNumber(objId) {
var item = getObj(objId);
var result = item.value;
if (!validateNumber(objId))
return false;
if (result < 0) {
alert("The value must be a positive value.");
item.focus();
return false;
}
return true;
}
</script>
<script>
function PanicStop()
{
// srn - do not ask for confirmation
//if (!confirm("Are you sure you want to stop all process?"))
// return;
var f = document.stop_form;
f.submit();
}
</script>
<form name="stop_form" action="index.php" method="post">
<input type="hidden" name="cmd" value="stop">
</form>
<!-- Button menuing system added 8/2/2009 by gbr -->
<table border="0" cellpadding="0" cellspacing="0" width=100%><tr><td>
<a href="index.php?cmd=scan"
onmouseover="setOverImg('2','');overSub=true;showSubMenu('submenu2','button2');"
onmouseout="setOutImg('2','');overSub=false;setTimeout('hideSubMenu(\'submenu2\')',delay);" target="">
<img src="buttons/button2up.png" border="0" id="button2" vspace="0" hspace="0"></a>
<a href="index.php?cmd=about&mode=photos"
onmouseover="setOverImg('5','');overSub=true;showSubMenu('submenu5','button5');"
onmouseout="setOutImg('5','');overSub=false;setTimeout('hideSubMenu(\'submenu5\')',delay);" target="">
<img src="buttons/button5up.png" border="0" id="button5" vspace="0" hspace="0"></a>
<a href="index.php?cmd=about"
onmouseover="setOverImg('6','');overSub=true;showSubMenu('submenu6','button6');"
onmouseout="setOutImg('6','');overSub=false;setTimeout('hideSubMenu(\'submenu6\')',delay);" target="">
<img src="buttons/button6up.png" border="0" id="button6" vspace="0" hspace="0"></a>
<a href="index.php?cmd=movies"
onmouseover="setOverImg('9','');overSub=true;showSubMenu('submenu9','button9');"
onmouseout="setOutImg('9','');overSub=false;setTimeout('hideSubMenu(\'submenu9\')',delay);" target="">
<img src="buttons/movies_up.png" border="0" id="button9" vspace="0" hspace="0"></a>
<a href="index.php?cmd=logoff"
onmouseover="setOverImg('10','');overSub=true;showSubMenu('submenu10','button10');"
onmouseout="setOutImg('10','');overSub=false;setTimeout('hideSubMenu(\'submenu10\')',delay);" target="">
<img src="buttons/button9up.png" border="0" id="button10" vspace="0" hspace="0"></a><br>
</td></tr></table>
<script>
function explainWhyNot()
{
document.getElementById("WhyNot").innerHTML="<CENTER><font color=red>Cannot connect to the RootView server you selected.</font></CENTER><br>The RootView server you selected is supposed to be a service running on the machine at IP address 205.149.147.130 and Port 17026. The possible problems and suggested solutions are:<br><br>1. The host machine 205.149.147.130 is not running, or is not connected to the internet. Try pinging 205.149.147.130.<br><br>2. The RootView service is not started, or paused. On the host machine 205.149.147.130, log in as the Administrator. Right click on My Computer, and click on Manage|Services. Find RootView Service in the list of services. In the Status column of the Services window, confirm that the service is started. If the service is stopped or paused, Start the service.<br><br>3. The Rootview service is attempting to open the wrong port, that is, not opening the socket at port 17026. This can be checked by opening the rootviewsrv.ini file in the same directory at the rootviewsrv.exe file and looking for the section [TCPIP]. In that section check the Port value is 17026. If it is incorrect, change it to the correct port, save the file, and restart the service.<br><br>4. The firewall on the computer that is running the service is blocking port 17026. Check Start|Control Panel|Firewall and click on the Exceptions tab. Check for a RootView entry. Select it and click on Edit to view the port number. Make sure that you have a RootView entry with a Port number that equals 17026.<br><br>";
}
</script><TABLE ALIGN=CENTER><TR><TD ALIGN=CENTER><div class="header">BW3-20 [AMR-26] <FONT SIZE="-1">v3.0.0.33</font></div><div class="header_desc"></div><font size='+1'>(Scan)</font></TD><TD> &nbsp; &nbsp;</TD><TD></TD></TR></TABLE>
<table>
<tr>
<td valign=top>
<table cellpassing=0 cellspacing=0 border=0>
<form name="filterform" method="POST" action="index.php">
<input type="hidden" name="cmd" value="scan">
<input type="hidden" name="start" value="0">
<input type="hidden" name="order" value="0">
<input type="hidden" name="order_dir" value="1">
<!-- labels for edit controls in first row. 2/7/2011 gbr -->
<tr>
<td VALIGN=TOP>Filter Scans:&nbsp;</td>
<td VALIGN=TOP>&nbsp;
<td VALIGN=TOP>&nbsp;User:</td>
<td VALIGN=TOP>&nbsp;Date From:</td>
<td VALIGN=TOP>&nbsp;Date To:</td>
<TD VALIGN=TOP>From Scan ID</TD> <!-- added 2/7/2011 gbr per block 51 -->
<TD VALIGN=TOP>To Scan ID</TD> <!-- added 2/7/2011 gbr per block 51 -->
<td VALIGN=TOP>&nbsp;Scans per page:</td>
</tr>
<!-- edit controls in second row. 2/7/2011 gbr -->
<tr>
<TD>&nbsp;</TD>
<td VALIGN=TOP>
<input type="hidden" name="FilterScanStatus" value="2">
&nbsp;</td>
<td VALIGN=TOP><select name="FilterUser">
<option value="0">All</option>
<option value="1">yuri</option>
<option value="2">George Rothbart</option>
<option value="3">Mike Taggart</option>
<option value="23">Joanne Childs</option>
<option value="7">Mike Allen</option>
<option value="14">Tom Unwin</option>
<option value="26" selected>Mark/Kyle</option>
<option value="22">Colleen Iverson</option>
</select></td>
<td VALIGN=TOP><input type=hidden name=hidedate value=""><input type="text" name="FilterDtFrom" id="FilterDtFrom" value="" size=10 maxlength=10 ><a href="javascript:void(0)" onclick="gfPop.fStartPop(document.filterform.FilterDtFrom,document.filterform.hidedate);return false;" HIDEFOCUS><img name="popcal" align="absmiddle" src="js/popcalendarrange/calbtn.gif" width="34" height="22" border="0" alt="date"></a></TD>
<td VALIGN=TOP><input type="text" name="FilterDtTo" id="FilterDtTo" value="" size=10 maxlength=10 ><a href="javascript:void(0)" onclick="gfPop.fStartPop(document.filterform.FilterDtTo,document.filterform.hidedate);return false;" HIDEFOCUS><img name="popcal" align="absmiddle" src="js/popcalendarrange/calbtn.gif" width="34" height="22" border="0" alt="date"></a> <TD VALIGN=TOP><INPUT name="FilterIdFrom" value=0 SIZE=9></TD><TD VALIGN=TOP><INPUT name="FilterIdTo" value=0 SIZE=9></TD> <td VALIGN=TOP><select name="FilterCount">
<option value="20" selected>20</option>
<option value="40">40</option>
<option value="80">80</option>
<option value="160">160</option>
<option value="320">320</option>
</select></td>
<td VALIGN=TOP><input type=submit value=" Go "></td>
</tr>
</form>
</table>
</td>
<td width=100>&nbsp;&nbsp;</td>
<td>
<FIELDSET>
<LEGEND>Comparison Mode</LEGEND>
<table cellpadding=0 cellspacing=0 border=0>
<form name="compareform" method="POST" action="index.php" onsubmit="return validateScanList();">
<input type="hidden" name="cmd" value="scan">
<input type="hidden" name="mode" value="compare">
<input type="hidden" name="scanList" value="">
<tr><td align=center><table cellpadding=0 cellspacing=0 border=0>
<tr>
<td><input type="RADIO" name="cmp_mode" value="0" CHECKED></td>
<td align='left'>Fit To Screen</td>
</tr>
<tr>
<td><input type="RADIO" name="cmp_mode" value="1"></td>
<td align='left'>Normal Size</td>
</tr>
</table></td></tr>
<tr><td align=center><input type="submit" value=" Compare ">
</form>
</td></tr>
</table>
</FIELDSET>
</td>
</tr>
</table>
<!-- PopCalendar(tag name and id must match) Tags should sit at the page bottom -->
<iframe width=174 height=189 name="gToday:normal:agenda.js" id="gToday:normal:agenda.js"
src="js/popcalendarrange/ipopeng_001.htm" scrolling="no" frameborder="0"
style="visibility:visible; z-index:999; position:absolute; left:-500px; top:0px;">
</iframe>
<p align=center>
First&nbsp;
Previous&nbsp;
1&nbsp;
<a href="JavaScript: SetPage(20);">2</a>&nbsp;
<a href="JavaScript: SetPage(20);">Next</a>&nbsp;
<a href="JavaScript: SetPage(20);">Last</a>&nbsp;
</p>
<script>
function DeleteScan(id, name)
{
if (!confirm("Are you sure you want to delete \""+name+"\" scan?"))
return;
var frm = document.deleteform;
frm.id.value=id;
frm.submit();
}
function SetOrder(new_order)
{
// current order and direction
order = 0;
order_dir = 1;
// if same order then change direction
if (order==new_order)
order_dir = (order_dir==0 ? 1 : 0);
else {
order = new_order;
order_dir = 0;
}
// update the screen
var f = document.filterform;
f.order.value = order;
f.order_dir.value = order_dir;
f.submit();
}
function SetPage(n)
{
var f = document.filterform;
f.start.value = n;
f.submit();
}
function processScanId(current_state, scanID) {
var scan = scanID.toString();
var f = document.compareform;
var str = f.scanList.value;
if ((current_state == true) && (str.indexOf(" "+ scan) == -1)) {
str = str + " " + scan;
f.scanList.value = str;
}
if ((current_state == false) && (str.indexOf(" "+ scan) > -1)) {
str = str.replace(" "+ scan, "");
f.scanList.value = str;
}
//alert("ScanID(s)=>" + str+"<, current state is "+ (current_state? "checked" : "unchecked"));
}
function validateScanList() {
var f = document.compareform;
var str = f.scanList.value;
if ((str == '') || (str.indexOf(" ") == str.lastIndexOf(" "))) {
alert("Please select at least 2 scans for comparison!");
return false;
}
//alert("Selected scans for comparison: "+f.scanList.value);
return true;
}
</script>
<table cellpadding=1 cellspacing=0 border="0" bgcolor="#000000" width="100%">
<form action="index.php" name="deleteform" method="post">
<input type="hidden" name="cmd" value="scan">
<input type="hidden" name="mode" value="delete">
<input type="hidden" name="id" value="">
</form>
<tr><td>
<table cellspacing=0 cellpadding=3 border=0 width="100%">
<tr class="data_title">
<td><table cellpadding=0 cellspacing=0 border=0><tr><td><a class=header_link href="JavaScript: SetOrder(0);">ID</a></td>
<td>&nbsp;<img src="images/order_desc.gif" width=18 height=18 border=0></td>
</tr></table></td>
<td><a class=header_link href="JavaScript: SetOrder(1);">Name</a></td>
<td><a class=header_link href="JavaScript: SetOrder(2);">Scan Time</a></td>
<td><a class=header_link href="JavaScript: SetOrder(3);">Step Units</a></td>
<td><a class=header_link href="JavaScript: SetOrder(4);">(X,Y)-(X,Y)-(DX,DY)</a></td>
<td><a class=header_link href="JavaScript: SetOrder(5);">Dwell Time, ms</a></td>
<td><a class=header_link href="JavaScript: SetOrder(6);">Scan Lines</a></td>
<td><a class=header_link href="JavaScript: SetOrder(7);">Scan Mode</a></td>
<td><a class=header_link href="JavaScript: SetOrder(8);">Start Time</a></td>
<td><a class=header_link href="JavaScript: SetOrder(9);">End Time</a></td>
<td><a class=header_link href="JavaScript: SetOrder(10);">Can- celled</a></td>
<td><a class=header_link href="JavaScript: SetOrder(11);">User</a></td>
<td>Scan Status</td>
<td>Arc- hived</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr class="data_even">
<td>158374</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-07-29 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-07-29 04:59:46</td>
<td>2024-07-30 02:51:07</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=158374">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare0" value="158374" onclick='processScanId(this.checked, 158374);'> </td>
</tr>
<tr class="data_odd">
<td>158222</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-07-22 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-07-22 04:59:36</td>
<td>2024-07-23 02:50:57</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=158222">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare1" value="158222" onclick='processScanId(this.checked, 158222);'> </td>
</tr>
<tr class="data_even">
<td>158069</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-07-15 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-07-15 05:00:19</td>
<td>2024-07-16 02:51:26</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=158069">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare2" value="158069" onclick='processScanId(this.checked, 158069);'> </td>
</tr>
<tr class="data_odd">
<td>157971</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-07-08 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-07-08 04:59:48</td>
<td>2024-07-09 02:51:10</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=157971">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare3" value="157971" onclick='processScanId(this.checked, 157971);'> </td>
</tr>
<tr class="data_even">
<td>157813</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-07-01 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-07-01 05:00:17</td>
<td>2024-07-02 02:51:29</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=157813">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare4" value="157813" onclick='processScanId(this.checked, 157813);'> </td>
</tr>
<tr class="data_odd">
<td>157656</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-06-24 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-06-24 05:00:17</td>
<td>2024-06-25 02:49:49</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=157656">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare5" value="157656" onclick='processScanId(this.checked, 157656);'> </td>
</tr>
<tr class="data_even">
<td>157498</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-06-17 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-06-17 05:00:19</td>
<td>2024-06-18 02:51:50</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=157498">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare6" value="157498" onclick='processScanId(this.checked, 157498);'> </td>
</tr>
<tr class="data_odd">
<td>157340</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-06-10 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-06-10 05:00:18</td>
<td>2024-06-11 02:51:48</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=157340">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare7" value="157340" onclick='processScanId(this.checked, 157340);'> </td>
</tr>
<tr class="data_even">
<td>157091</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-06-03 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-06-03 04:59:54</td>
<td>2024-06-04 02:46:57</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=157091">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare8" value="157091" onclick='processScanId(this.checked, 157091);'> </td>
</tr>
<tr class="data_odd">
<td>156743</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-05-27 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-05-27 04:59:56</td>
<td>2024-05-28 02:44:55</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=156743">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare9" value="156743" onclick='processScanId(this.checked, 156743);'> </td>
</tr>
<tr class="data_even">
<td>156416</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-05-20 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-05-20 04:59:33</td>
<td>2024-05-21 02:46:29</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=156416">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare10" value="156416" onclick='processScanId(this.checked, 156416);'> </td>
</tr>
<tr class="data_odd">
<td>156089</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-05-13 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-05-13 05:00:02</td>
<td>2024-05-14 02:46:12</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=156089">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare11" value="156089" onclick='processScanId(this.checked, 156089);'> </td>
</tr>
<tr class="data_even">
<td>155763</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-05-06 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-05-06 05:00:08</td>
<td>2024-05-07 02:46:49</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=155763">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare12" value="155763" onclick='processScanId(this.checked, 155763);'> </td>
</tr>
<tr class="data_odd">
<td>155391</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-04-29 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-04-29 05:00:11</td>
<td>2024-04-30 02:46:58</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=155391">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare13" value="155391" onclick='processScanId(this.checked, 155391);'> </td>
</tr>
<tr class="data_even">
<td>154869</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-04-22 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-04-22 04:59:48</td>
<td>2024-04-23 02:45:46</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=154869">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare14" value="154869" onclick='processScanId(this.checked, 154869);'> </td>
</tr>
<tr class="data_odd">
<td>154416</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-04-15 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-04-15 04:59:31</td>
<td>2024-04-16 02:46:16</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=154416">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare15" value="154416" onclick='processScanId(this.checked, 154416);'> </td>
</tr>
<tr class="data_even">
<td>153954</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-04-08 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-04-08 04:59:37</td>
<td>2024-04-09 02:45:47</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=153954">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare16" value="153954" onclick='processScanId(this.checked, 153954);'> </td>
</tr>
<tr class="data_odd">
<td>153488</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-04-01 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-04-01 05:00:01</td>
<td>2024-04-02 02:44:44</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=153488">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare17" value="153488" onclick='processScanId(this.checked, 153488);'> </td>
</tr>
<tr class="data_even">
<td>153018</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-03-25 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-03-25 05:00:07</td>
<td>2024-03-26 02:46:29</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=153018">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare18" value="153018" onclick='processScanId(this.checked, 153018);'> </td>
</tr>
<tr class="data_odd">
<td>152549</td>
<td>Plot 20 AMR26 Full Tube Scan </td>
<td>2024-03-18 05:00</td>
<td>mm</td>
<td>(0,0)-(310,740)- (3.01,2.26)</td>
<td>100</td>
<td>Horizontal</td>
<td>Raster</td>
<td>2024-03-18 04:59:30</td>
<td>2024-03-19 02:44:26</td>
<td align="center">0</td>
<td>SPRUCE</td>
<td>Completed</td>
<td align="center" style="cursor:help" title="All Images archived, Mosaic archived">X </td>
<td><a href="index.php?cmd=scan&mode=view&id=152549">View</a>&nbsp;&nbsp;</td>
<td><input type="CHECKBOX" name="compare19" value="152549" onclick='processScanId(this.checked, 152549);'> </td>
</tr>
</table>
</td></tr>
</table>
<p align=center>
First&nbsp;
Previous&nbsp;
1&nbsp;
<a href="JavaScript: SetPage(20);">2</a>&nbsp;
<a href="JavaScript: SetPage(20);">Next</a>&nbsp;
<a href="JavaScript: SetPage(20);">Last</a>&nbsp;
</p>
+663
View File
@@ -0,0 +1,663 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Root View</title>
<!-- <script src="menuscript.js" language="javascript" type="text/javascript"></script> -->
<link rel="stylesheet" type="text/css" href="menustyle.css" media="screen, print" />
<script src="menuscript.js" language="javascript" type="text/javascript"></script>
</head>
<body>
<style>
P {
font: 9pt Arial, Helvetica, sans-serif;
}
.header {
font: bold 22pt Arial, Helvetica, sans-serif;
color: #008080;
text-align: center;
}
.header_desc {
font: bold 10pt Arial, Helvetica, sans-serif;
color: #800000;
text-align: center;
}
.error {
font: bold 16pt Arial, Helvetica, sans-serif;
color: #ff0000;
text-align: center;
}
.information {
font: 10pt Arial, Helvetica, sans-serif;
color: #0000ff;
text-align: left;
}
.data_title {
font: bold 9pt Arial, Helvetica, sans-serif;
color: white;
background: #696969;
text-align: left;
}
.data_odd {
font: 9pt Arial, Helvetica, sans-serif;
font-weight : lighter;
background-color : #eeeeee;
color : #000000;
}
.data_even {
font: 9pt Arial, Helvetica, sans-serif;
font-weight : lighter;
background-color : #ffffff;
color : #000000;
}
.data {
font: 9pt Arial, Helvetica, sans-serif;
color: #000000;
}
.login {
font: bold 9pt Arial, Helvetica, sans-serif;
}
.header_link {
font: bold 9pt Arial, Helvetica, sans-serif;
color: white;
background: #696969;
text-align: left;
text-decoration: none;
}
#pointer_div {
position:relative;
border-style:solid;
border-width:2px;
border-color:red;
margin:5px;
/*cursor:crosshair;*/
}
#data_left {
text-align: left;
}
#image_frame {
background-color: #70a0b0;
font-size:2px;
}
.red_text {
color: #ff0000;
font: bold 9pt Arial, Helvetica, sans-serif;
}
</style>
<script>
function validateFilename(filenameID) {
var item = getObj(filenameID);
var result = item.value;
// based on code found on http://www.codingforums.com/showthread.php?t=194468
if (/^[a-z0-9\.\s\-\_\[\]]*$/i.test(result) === false) { // anything but a-zA-Z0-9, [,], dot, hypen, space, underscore is disallowed
alert ("File name " + result + " contains invalid character(s)! \r\nAnything but a-z, A-Z, 0-9, square brackets, dot, hypen, space, underscore is disallowed");
item.focus();
return false;
}
// If we allow using spaces in filenames, we should at least strip any leading or trailing spaces
item.value = item.value.replace(/^\s+|\s+$/g,""); // the g switch is essential!!
return true;
}
function getObj(ElementId)
{
if (document.getElementById) // BYI now this method works perfect for both IE and NS
return document.getElementById(ElementId);
return false;
}
function showit(ElementId)
{
getObj(ElementId).style.display='block';
}
function hideit(ElementId)
{
getObj(ElementId).style.display='none';
}
function FormatFloat(v)
{
if (v=="")
return v;
return Math.round(v*100)/100;
}
function validateNumber(objId){
// BYI the function validates if the value of the data entry element on the form is an integer or a float
// an alert message is popped up in case of error, the problem field is focused
// the function returns false so that if used in OnSubmit clause it will force the user to re-enter the data
var item = getObj(objId);
var result = item.value;
if (isNaN(Number(result))) {
alert("The value must be an integer or float value.");
item.focus();
return false;
}
return true;
}
function validatePositiveNumber(objId) {
var item = getObj(objId);
var result = item.value;
if (!validateNumber(objId))
return false;
if (result < 0) {
alert("The value must be a positive value.");
item.focus();
return false;
}
return true;
}
</script>
<script>
function PanicStop()
{
// srn - do not ask for confirmation
//if (!confirm("Are you sure you want to stop all process?"))
// return;
var f = document.stop_form;
f.submit();
}
</script>
<form name="stop_form" action="index.php" method="post">
<input type="hidden" name="cmd" value="stop">
</form>
<!-- Button menuing system added 8/2/2009 by gbr -->
<table border="0" cellpadding="0" cellspacing="0" width=100%><tr><td>
<a href="index.php?cmd=scan"
onmouseover="setOverImg('2','');overSub=true;showSubMenu('submenu2','button2');"
onmouseout="setOutImg('2','');overSub=false;setTimeout('hideSubMenu(\'submenu2\')',delay);" target="">
<img src="buttons/button2up.png" border="0" id="button2" vspace="0" hspace="0"></a>
<a href="index.php?cmd=about&mode=photos"
onmouseover="setOverImg('5','');overSub=true;showSubMenu('submenu5','button5');"
onmouseout="setOutImg('5','');overSub=false;setTimeout('hideSubMenu(\'submenu5\')',delay);" target="">
<img src="buttons/button5up.png" border="0" id="button5" vspace="0" hspace="0"></a>
<a href="index.php?cmd=about"
onmouseover="setOverImg('6','');overSub=true;showSubMenu('submenu6','button6');"
onmouseout="setOutImg('6','');overSub=false;setTimeout('hideSubMenu(\'submenu6\')',delay);" target="">
<img src="buttons/button6up.png" border="0" id="button6" vspace="0" hspace="0"></a>
<a href="index.php?cmd=movies"
onmouseover="setOverImg('9','');overSub=true;showSubMenu('submenu9','button9');"
onmouseout="setOutImg('9','');overSub=false;setTimeout('hideSubMenu(\'submenu9\')',delay);" target="">
<img src="buttons/movies_up.png" border="0" id="button9" vspace="0" hspace="0"></a>
<a href="index.php?cmd=logoff"
onmouseover="setOverImg('10','');overSub=true;showSubMenu('submenu10','button10');"
onmouseout="setOutImg('10','');overSub=false;setTimeout('hideSubMenu(\'submenu10\')',delay);" target="">
<img src="buttons/button9up.png" border="0" id="button10" vspace="0" hspace="0"></a><br>
</td></tr></table>
<p class="header">"Plot 20 AMR26 Full Tube Scan " Scan View</p>
<script type="text/javascript" src="js/prototype.js" ></script>
<script type="text/javascript" >
function getcordsInDiv(e){
//get the position of the container
var containerLeft = Position.page($('pointer_div'))[0];
var absLeft = Position.realOffset($('pointer_div'))[0];
var containerTop = Position.page($('pointer_div'))[1];
var absTop = Position.realOffset($('pointer_div'))[1];
//get the mouse coordinates
mouseX = Event.pointerX(e);
mouseY = Event.pointerY(e);
//calculate the absolute mouse position in the div
horizontalPosition = mouseX - containerLeft;
verticalPosition = mouseY - containerTop;
//use prototypes function to get the dimension
//this is a VERY usefull function because it also checks for borders
containerDimensions = $('pointer_div').getDimensions();
height = containerDimensions.height;
width = containerDimensions.width;
//check if the mouse is out or inside the div
// if(horizontalPosition < 0 || verticalPosition < 0 || mouseX > (width + containerLeft) || mouseY > (height + containerTop) ){
if(horizontalPosition < 0 || verticalPosition < 0 ){
TooltipString = 'hp='+horizontalPosition+',vp='+verticalPosition;
}else{
if (!RootDown) {
var xxx = Math.floor((horizontalPosition - absLeft)/33);
var yyy = Math.floor((height - verticalPosition + absTop - 4)/25);
var xx = Math.round(100*((horizontalPosition - absLeft)/33*3.01+0))/100;
var yy = Math.round(100*((height - verticalPosition + absTop - 4)/25*2.26+0))/100;
}
else {
var xxx = Math.floor((width - horizontalPosition + absLeft - 4)/33);
var yyy = Math.floor((verticalPosition - absTop)/25);
var xx = Math.round(100*((width - horizontalPosition + absLeft - 4)/33*3.01+0))/100;
var yy = Math.round(100*((verticalPosition - absTop)/25*2.26+0))/100;
}
TooltipString = 'Tiles X=' + xxx + ', Y=' + yyy + '<br>'+
// 'h=' + height + ', w=' + width + ', tt=' + (absTop+containerTop);
'Xmm=' + xx + ', Ymm=' + yy + '<br>' +
'(Depth undefined offline)';
}
}
Event.observe(document, 'mousemove', getcordsInDiv);
var TooltipString = '';
var tooltip = {
options: {
attr_name: "tooltip",
blank_text: "(????????? ? ????? ????)",
newline_entity: " ",
max_width: 0,
delay: 100,
skip_tags: ["link", "style"]
},
t: document.createElement("DIV"),
c: null,
g: false,
canvas: null,
m: function(e){
if (tooltip.g){
var x = window.event ? event.clientX + (tooltip.canvas.scrollLeft || document.body.scrollLeft) : e.pageX;
var y = window.event ? event.clientY + (tooltip.canvas.scrollTop || document.body.scrollTop) : e.pageY;
tooltip.a(x, y);
}
},
d: function(){
tooltip.canvas = document.getElementsByTagName(document.compatMode && document.compatMode == "CSS1Compat" ? "HTML" : "BODY")[0];
tooltip.t.setAttribute("id", "tooltip");
document.body.appendChild(tooltip.t);
if (tooltip.options.max_width) tooltip.t.style.maxWidth = tooltip.options.max_width + "px"; // all but ie
var a = document.all && !window.opera ? document.all : document.getElementsByTagName("*"); // in opera 9 document.all produces type mismatch error
var l = a.length;
for (var i = 0; i < l; i++){
if (!a[i] || tooltip.options.skip_tags.in_array(a[i].tagName.toLowerCase())) continue;
var tooltip_title = a[i].getAttribute("title"); // returns form object if IE & name="title"; then IE crashes; so...
if (tooltip_title && typeof tooltip_title != "string") tooltip_title = "";
var tooltip_alt = a[i].getAttribute("alt");
var tooltip_blank = a[i].getAttribute("target") && a[i].getAttribute("target") == "_blank" && tooltip.options.blank_text;
if (tooltip_title || tooltip_blank){
a[i].setAttribute(tooltip.options.attr_name, tooltip_blank ? (tooltip_title ? tooltip_title + " " + tooltip.options.blank_text : tooltip.options.blank_text) : tooltip_title);
if (a[i].getAttribute(tooltip.options.attr_name)){
a[i].removeAttribute("title");
if (tooltip_alt && a[i].complete) a[i].removeAttribute("alt");
tooltip.l(a[i], "mouseover", tooltip.s);
tooltip.l(a[i], "mouseout", tooltip.h);
}
}else if (tooltip_alt && a[i].complete){
a[i].setAttribute(tooltip.options.attr_name, tooltip_alt);
if (a[i].getAttribute(tooltip.options.attr_name)){
a[i].removeAttribute("alt");
tooltip.l(a[i], "mouseover", tooltip.s);
tooltip.l(a[i], "mouseout", tooltip.h);
}
}
if (!a[i].getAttribute(tooltip.options.attr_name) && tooltip_blank){
//
}
}
document.onmousemove = tooltip.m;
window.onscroll = tooltip.h;
tooltip.a(-99, -99);
},
_: function(s){
s = s.replace(/\&/g,"&amp;");
s = s.replace(/\</g,"&lt;");
s = s.replace(/\>/g,"&gt;");
return s;
},
s: function(e){
if (typeof tooltip == "undefined") return;
var d = window.event ? window.event.srcElement : e.target;
if (!d.getAttribute(tooltip.options.attr_name)) return;
var s = d.getAttribute(tooltip.options.attr_name);
if (tooltip.options.newline_entity){
var s = tooltip._(s);
s = s.replace(eval("/" + tooltip._(tooltip.options.newline_entity) + "/g"), "<br />");
tooltip.t.innerHTML = s;
}else{
if (tooltip.t.firstChild) tooltip.t.removeChild(tooltip.t.firstChild);
tooltip.t.appendChild(document.createTextNode(s));
}
tooltip.c = setTimeout(function(){
tooltip.t.style.visibility = 'visible';
}, tooltip.options.delay);
tooltip.g = true;
},
h: function(e){
if (typeof tooltip == "undefined") return;
tooltip.t.style.visibility = "hidden";
if (!tooltip.options.newline_entity && tooltip.t.firstChild) tooltip.t.removeChild(tooltip.t.firstChild);
clearTimeout(tooltip.c);
tooltip.g = false;
tooltip.a(-99, -99);
},
l: function(o, e, a){
if (o.addEventListener) o.addEventListener(e, a, false); // was true--Opera 7b workaround!
else if (o.attachEvent) o.attachEvent("on" + e, a);
else return null;
},
a: function(x, y){
var w_width = tooltip.canvas.clientWidth ? tooltip.canvas.clientWidth + (tooltip.canvas.scrollLeft || document.body.scrollLeft) : window.innerWidth + window.pageXOffset;
var w_height = window.innerHeight ? window.innerHeight + window.pageYOffset : tooltip.canvas.clientHeight + (tooltip.canvas.scrollTop || document.body.scrollTop); // should be vice verca since Opera 7 is crazy!
if (document.all && document.all.item && !window.opera) tooltip.t.style.width = "300px"; //tooltip.options.max_width && tooltip.t.offsetWidth > tooltip.options.max_width ? tooltip.options.max_width + "px" : "auto";
var t_width = tooltip.t.offsetWidth;
var t_height = tooltip.t.offsetHeight;
tooltip.t.style.left = x + 15 + "px";
tooltip.t.style.top = y + 8 + "px";
if (x + t_width > w_width) tooltip.t.style.left = -10 + w_width - t_width + "px";
if (y + t_height > w_height) tooltip.t.style.top = -45 + w_height - t_height + "px";
tooltip.t.innerHTML = TooltipString;
tooltip.t.style.width = 210 + "px";
// should be 55 instead of 40 for one more line
tooltip.t.style.height = 55 + "px";
}
}
Array.prototype.in_array = function(value){
var l = this.length;
for (var i = 0; i < l; i++)
if (this[i] === value) return true;
return false;
};
var root = window.addEventListener || window.attachEvent ? window : document.addEventListener ? document : null;
if (root){
if (root.addEventListener) root.addEventListener("load", tooltip.d, false);
else if (root.attachEvent) root.attachEvent("onload", tooltip.d);
}
</script>
<style>
#tooltip{
background:#FFFF00;
border:1px solid #666666;
color:#333333;
font:menu;
margin:0px;
padding:3px 5px;
position:absolute;
visibility:hidden;
width : 180 px;
height : 35 px;
}
</style>
<script type=text/javascript>
var ScaleFactor = 1;
function show_tile(x,y)
{
var TilePopup=window.open("include/tile_view.php?cmd=image&mode=image_scan&id=158374&sX=0&sY=0&eX=310&eY=740&dX=3.01&dY=2.26&rd=1&sz=1"+"&s="+ScaleFactor+"&x="+x+"&y="+y+"&Tx="+Tx+"&Ty="+Ty, 'RTL_window',"width=670,height=540,left=200,top=150, titlebar=no,location=no,toolbar=no");
TilePopup.focus();
}
</script>
<script type="text/javascript"> <!--
var tt, ll, bb, rr = "";
var RootDown = 1;
running = 1;
var t, l, b, r = "";
var Tx = -1,
Ty = -1;
function clearTile() {
if ($('my_pointer') )
$('pointer_div').removeChild($('my_pointer'));
}
function hiliteTile() {
var xTile, yTile;
var rootGrowsDown;
xTile = $('xTile').value;
yTile = $('yTile').value;
rootGrowsDown = 1;
if (validatePositiveNumber('xTile')) {
xx = 102;
if (xTile > xx) {
alert("The max value for X tile is "+ xx+" (we count from zero tile)");
$('xTile').focus();
return false;
}
}
else
return false;
if (validatePositiveNumber('yTile')) {
yy = 327;
if (yTile > yy) {
alert("The max value for Y tile is "+ yy+" (we count from zero tile)");
$('yTile').focus();
return false;
}
}
else
return false;
clearTile();
my_pointer = document.createElement("div");
my_pointer.setAttribute("id","my_pointer");
if (rootGrowsDown)
my_pointer.setAttribute("style", "background-color:#6ff;filter:alpha(opacity=75);opacity:0.75;width:17px;height:9px;border:3px solid #f00;position:absolute;top:" + (25*yTile + 5) + "px;left:" + (33*(102- xTile)+5) + "px;");
else
my_pointer.setAttribute("style", "background-color:#6ff;filter:alpha(opacity=75);opacity:0.75;width:17px;height:9px;border:3px solid #f00;position:absolute;top:"+(25*(327- yTile)+5)+"px;left:"+(33*xTile+5)+"px;");
$('pointer_div').appendChild(my_pointer);
return false;
}
function point_it(event){
pos_x = event.offsetX ? (event.offsetX) : event.pageX - document.getElementById("pointer_div").offsetLeft;
pos_y = event.offsetY?(event.offsetY):event.pageY - document.getElementById("pointer_div").offsetTop;
ll = pos_x;
tt = pos_y;
rr = pos_x;
bb = pos_y;
fx(pos_y,pos_x,0);
}
function fx(top,left, FirstTime){
if (running == 2){
current = document.getElementById('pointer');
current.parentNode.removeChild(current);
running = 1;
}
if (running == 1){
t=top;
l=left;
b=top;
r=left;
// now round up the rectangle to the covering tiles
Tx = Math.floor(l/33);
Ty = Math.floor(t/25);
t = 25* Ty;
l = 33* Tx;
b = 25* Math.ceil(b/25);
r = 33* Math.ceil(r/33);
if (RootDown == 0) {
Ty = 328 - Ty - 1;
rectL = Math.floor(ll/33)*3.01+0;
rectB = 2.26*(328 - Math.floor(bb/25) -1)+0;
}
else {
rectL = 3.01*(103 - Math.floor(ll/33) -1)+0;
rectB = Math.floor(bb/25)*2.26+0;
}
// Tx = Math.round(rectL/3.01);
// Ty = Math.round(rectB/2.26);
element = document.createElement("div");
element.setAttribute("id","pointer");
// element.setAttribute("style", "background-color:#fff;filter:alpha(opacity=75);opacity:0.75;width:"+(r-l)+"px;height:"+(b-t)+"px;border:1px solid #f00;position:relative;top:"+(t-8200-7)+"px;left:"+(l-2)+"px;");
element.setAttribute("style", "background-color:#fff;filter:alpha(opacity=75);opacity:0.75;width:"+(r-l-22)+"px;height:"+(b-t-18)+"px;border:4px solid #0F0;position:relative;top:"+(t-8200)+"px;left:"+(l+6)+"px;");
document.getElementById('pointer_div').appendChild(element);
running=2;
if (!FirstTime)
show_tile(rectL,rectB);
}
}
function showSubscanPage() {
subscanURL = 'index.php?cmd=scan&mode=subscan&id=158374';
if (Tx != -1)
subscanURL = subscanURL + "&Tx=" + Tx + "&Ty=" + Ty;
window.location.href=subscanURL;
}
//-->
</script>
<center>
<table id="data_left" cellpadding=0 cellspacing=3 border=0 width="100%">
<tr><td valign="top" width="35%">
<table cellpadding=0 cellspacing=3 border=0>
<tr><td>Scan ID:</td><td>158374</td></tr>
<tr><td>Name:</td><td>Plot 20 AMR26 Full Tube Scan </td></tr>
<tr><td>Scan Time:</td><td>2024-07-29 05:00:00</td></tr>
<tr><td>Starting X:</td><td>0 mm</td></tr>
<tr><td>Starting Y:</td><td>0 mm</td></tr>
<tr><td>Ending X:</td><td>310 mm</td></tr>
<tr><td>Ending Y:</td><td>740 mm</td></tr>
<tr><td>DX:</td><td>3.01 mm</td></tr>
<tr><td>DY:</td><td>2.26 mm</td></tr>
<tr><td>Dwell Time:</td><td>100 ms</td></tr>
<tr><td>Scan Lines:</td><td>Horizontal</td></tr>
<tr><td>Scan Mode:</td><td>Raster</td></tr>
<tr><td>Step Units:</td><td>mm</td></tr>
<tr><td>Start Time:</td><td>2024-07-29 04:59:46</td></tr>
<tr><td>End Time:</td><td>2024-07-30 02:51:07</td></tr>
<tr><td>Scan Status:&nbsp;&nbsp;&nbsp;</td><td>Completed</td></tr>
<tr><td>Root grows down:&nbsp;&nbsp;</td><td>Yes</td></tr>
<tr><td>Notes:</td><td></td></tr>
<tr><td>User:</td><td>SPRUCE</td></tr>
<tr><td>Total number of images:</td><td>33784 (103x328)</td>
</tr>
<tr><td>Total Disk Space:</td><td>1949.001 Mb</td></tr>
<tr><td>Total Travel distance:</td><td>204098.72 mm</td></tr>
<tr><td nowrap>Estimated Scan Time (HH:MM:SS):</td><td>
15:05:16 </td>
<tr>
<td>Scan Time (HH:MM:SS):</td>
<td>21:51:21</td>
</tr>
</table>
</td>
<td valign="top" width="65%">
<form method="post" name="imageform" action="index.php">
<input type="hidden" name="cmd" value="scan">
<input type="hidden" name="mode" value="view">
<input type="hidden" name="id" value="158374">
<input type="hidden" name="fZ" value="1">
<table cellpadding=0 cellspacing=0 border=0>
<tr>
<td>&nbsp;</td>
<td>Popup NxN:&nbsp;&nbsp;</td>
<td><select name="fS" onclick="ScaleFactor=this.value;">
<option value="1" selected>1X1</option>
<option value="2">2X2</option>
<option value="3">3X3</option>
<option value="4">4X4</option>
<option value="5">5X5</option>
<option value="6">6X6</option>
<option value="7">7X7</option>
</select></td>
<td>&nbsp;&nbsp;</td>
<td>&nbsp;
</td>
<td>&nbsp;</td>
<td>
<!-- this works just fine as a traditional submit button: <input type="button" value="Switch to Subscan Mode" onClick="window.location.href='index.php?cmd=scan&mode=subscan&id=158374'"> -->
<!-- however, this will use an image as a button, and does the same thing as the line above. gbr 10/3/2009 -->
<input type=image src="buttons\tosubscan.png" onClick="showSubscanPage(); return false;">
&nbsp;&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan=3>Highlight tile at X:&nbsp;
<input type="text" name="xTile" id="xTile" value="0" size="4" id="xTile">,&nbsp; Y:&nbsp;<input type="text" name="yTile" id="yTile" value="0" size="4" id="yTile"> &nbsp;
</td><td colspan=3><input type=image src="buttons\do_it_up.png" ID=toggler ONCLICK="hiliteTile(); return false;">&nbsp;<input type=image src="buttons\clear_up.png" ID=toggler1 ONCLICK="clearTile(); return false;"></td><td>&nbsp;</td>
</tr>
</table>
</form>
<div id="pointer_div" onclick="point_it(event)" style = "position:absolute;width:3399px;height:8200px;">
<img id ="container" title="Please wait while the mosaic is loaded completely..." src="http://205.149.147.131:8011/RootView_Database/158374/mosaic.jpg">
</div>
</td>
</tr>
</table>
<script type="text/javascript">
if (Tx != -1) {
if (1 == 0) {
ll = rr = Tx * 33 + 1;
tt = bb = (328 - Ty - 1) * 25 + 1;
}
else {
ll = rr = (103 - Tx -1) * 33 + 1;
tt = bb = Ty * 25 + 1;
}
fx(tt, ll, 1);
}
</script>
+153
View File
@@ -0,0 +1,153 @@
"""Tests for spruce.parsers — all pure, no network required."""
import pytest
from spruce.parsers import (
_grid_count,
_grid_values,
parse_machine_option,
parse_scan_row,
parse_scan_view,
)
# ---------------------------------------------------------------------------
# parse_machine_option
# ---------------------------------------------------------------------------
def test_parse_machine_option_basic():
raw = "BW3%2D20%20%5BAMR%2D26%5D|205.149.147.130|17026|26|8026|3.0.0.33|50.00"
m = parse_machine_option("BW3-20 [AMR-26]", raw)
assert m["label"] == "BW3-20 [AMR-26]"
assert m["machine_id"] == "26"
assert m["ip"] == "205.149.147.130"
assert m["version"] == "3.0.0.33"
assert m["option_value"] == raw
def test_parse_machine_option_short_value():
# Fewer pipe-delimited parts should not raise; missing fields return ""
m = parse_machine_option("Lonely Machine", "LM|10.0.0.1")
assert m["ip"] == "10.0.0.1"
assert m["machine_id"] == ""
assert m["port2"] == ""
# ---------------------------------------------------------------------------
# parse_scan_row
# ---------------------------------------------------------------------------
VALID_CELLS = [
"158374",
"Plot 20 AMR26 Full Tube Scan",
"2024-07-29 05:00",
"mm",
"(0,0)-(310,740)-(3.01,2.26)",
"100",
"Horizontal",
"Raster",
"2024-07-29 04:59:46",
"2024-07-30 02:51:07",
"0",
"SPRUCE",
"Completed",
"X",
]
def test_parse_scan_row_valid():
sc = parse_scan_row(VALID_CELLS)
assert sc is not None
assert sc["scan_id"] == 158374
assert sc["name"] == "Plot 20 AMR26 Full Tube Scan"
assert sc["status"] == "Completed"
assert sc["scan_time"] == "2024-07-29 05:00"
def test_parse_scan_row_ignores_non_digit_first_cell():
assert parse_scan_row(["ID", "Name", "Scan Time"]) is None
assert parse_scan_row(["Scan ID:", "158374"]) is None
def test_parse_scan_row_empty():
assert parse_scan_row([]) is None
def test_parse_scan_row_minimal():
sc = parse_scan_row(["42"])
assert sc is not None
assert sc["scan_id"] == 42
assert sc["name"] == ""
# ---------------------------------------------------------------------------
# parse_scan_view (uses fixture HTML from conftest.py)
# ---------------------------------------------------------------------------
def test_parse_scan_view_scan_id(scan_view_html):
meta = parse_scan_view(scan_view_html)
assert meta.get("scan_id") == 158374
def test_parse_scan_view_grid_dimensions(scan_view_html):
meta = parse_scan_view(scan_view_html)
assert meta.get("nx") == 103
assert meta.get("ny") == 328
def test_parse_scan_view_step_sizes(scan_view_html):
meta = parse_scan_view(scan_view_html)
assert meta.get("dx") == pytest.approx(3.01)
assert meta.get("dy") == pytest.approx(2.26)
def test_parse_scan_view_total_tiles(scan_view_html):
meta = parse_scan_view(scan_view_html)
# 103 × 328 = 33784
assert meta.get("total_tiles") == 103 * 328
def test_parse_scan_view_empty_string():
meta = parse_scan_view("")
assert meta == {}
# ---------------------------------------------------------------------------
# _grid_count
# ---------------------------------------------------------------------------
def test_grid_count_typical():
assert _grid_count(0, 310, 3.01) == 103
assert _grid_count(0, 740, 2.26) == 328
def test_grid_count_zero_step():
assert _grid_count(0, 100, 0) == 0
def test_grid_count_single():
assert _grid_count(0, 1, 1) == 1
# ---------------------------------------------------------------------------
# _grid_values
# ---------------------------------------------------------------------------
def test_grid_values_basic():
vals = _grid_values(0.0, 3, 3.01)
assert vals == [0.0, 3.01, 6.02]
def test_grid_values_empty():
assert _grid_values(0.0, 0, 1.0) == []
def test_grid_values_rounded():
# Floating-point accumulation should be rounded to 2 dp
vals = _grid_values(0.0, 4, 0.1)
for v in vals:
assert v == round(v, 2)
+102
View File
@@ -0,0 +1,102 @@
"""Tests for spruce.paths — pure path helpers, no network."""
from pathlib import Path
import pytest
from spruce.paths import _extract_date, machine_dir_name, mosaic_dest, tile_dest
MACHINE = {"label": "BW3-20 [AMR-26]", "machine_id": "26"}
SCAN_META = {"scan_time": "2024-07-29 05:00", "nx": 103, "ny": 328}
TILE = {"scan_id": 158374, "row_index": 0, "col_index": 2}
# ---------------------------------------------------------------------------
# machine_dir_name
# ---------------------------------------------------------------------------
def test_machine_dir_name_sanitises_brackets():
name = machine_dir_name({"label": "BW3-20 [AMR-26]"})
# Brackets and spaces replaced by underscores
assert "[" not in name
assert "]" not in name
assert " " not in name
def test_machine_dir_name_no_leading_trailing_underscores():
name = machine_dir_name({"label": "BW3-20 [AMR-26]"})
assert not name.startswith("_")
assert not name.endswith("_")
def test_machine_dir_name_stable():
# Same label should always produce the same dir name
assert machine_dir_name(MACHINE) == machine_dir_name(MACHINE)
# ---------------------------------------------------------------------------
# _extract_date
# ---------------------------------------------------------------------------
def test_extract_date_standard():
assert _extract_date("2024-07-29 05:00:00") == "2024-07-29"
def test_extract_date_date_only():
assert _extract_date("2024-07-29") == "2024-07-29"
def test_extract_date_no_date():
assert _extract_date("no date here") == "unknown"
def test_extract_date_empty():
assert _extract_date("") == "unknown"
# ---------------------------------------------------------------------------
# tile_dest
# ---------------------------------------------------------------------------
def test_tile_dest_structure(tmp_path):
dest = tile_dest(tmp_path, MACHINE, SCAN_META, TILE)
parts = dest.parts
assert str(TILE["scan_id"]) in parts
assert "tiles" in parts
assert dest.suffix == ".jpg"
assert "tile_r" in dest.name
def test_tile_dest_zero_padded(tmp_path):
# ny=328 → row index needs 3 digits; nx=103 → col index needs 3 digits
dest = tile_dest(tmp_path, MACHINE, SCAN_META, TILE)
# row_index=0 padded to 3 digits (max is 327) → "000"
assert "tile_r000_c" in dest.name
def test_tile_dest_contains_date(tmp_path):
dest = tile_dest(tmp_path, MACHINE, SCAN_META, TILE)
assert "2024-07-29" in str(dest)
# ---------------------------------------------------------------------------
# mosaic_dest
# ---------------------------------------------------------------------------
def test_mosaic_dest_filename(tmp_path):
dest = mosaic_dest(tmp_path, MACHINE, SCAN_META, 158374)
assert dest.name == "mosaic.jpg"
def test_mosaic_dest_contains_scan_id(tmp_path):
dest = mosaic_dest(tmp_path, MACHINE, SCAN_META, 158374)
assert "158374" in str(dest)
def test_mosaic_dest_contains_date(tmp_path):
dest = mosaic_dest(tmp_path, MACHINE, SCAN_META, 158374)
assert "2024-07-29" in str(dest)
+138
View File
@@ -0,0 +1,138 @@
"""Tests for spruce.progress — file I/O only, uses tmp_path."""
import csv
import json
from pathlib import Path
import pytest
from spruce.progress import CsvWriter, ProgressTracker
# ---------------------------------------------------------------------------
# ProgressTracker
# ---------------------------------------------------------------------------
def test_progress_mark_and_check(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
assert not p.is_done("http://example.com/a")
p.mark_done("http://example.com/a")
assert p.is_done("http://example.com/a")
def test_progress_roundtrip(tmp_path):
path = tmp_path / ".progress.json"
p = ProgressTracker(path)
p.mark_done("http://example.com/a")
p.mark_done("http://example.com/b")
p.save()
p2 = ProgressTracker(path)
assert p2.is_done("http://example.com/a")
assert p2.is_done("http://example.com/b")
assert not p2.is_done("http://example.com/c")
def test_progress_discard(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
p.mark_done("http://example.com/x")
assert p.is_done("http://example.com/x")
p.discard("http://example.com/x")
assert not p.is_done("http://example.com/x")
def test_progress_discard_nonexistent_is_noop(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
p.discard("http://example.com/never") # should not raise
def test_progress_iter_urls(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
p.mark_done("http://example.com/1")
p.mark_done("http://example.com/2")
assert set(p.iter_urls()) == {
"http://example.com/1",
"http://example.com/2",
}
def test_progress_len(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
assert len(p) == 0
p.mark_done("http://example.com/1")
assert len(p) == 1
p.mark_done("http://example.com/2")
assert len(p) == 2
p.discard("http://example.com/1")
assert len(p) == 1
def test_progress_save_creates_parent(tmp_path):
path = tmp_path / "nested" / "dir" / ".progress.json"
p = ProgressTracker(path)
p.mark_done("http://example.com/z")
p.save()
assert path.exists()
data = json.loads(path.read_text())
assert "http://example.com/z" in data["completed_urls"]
def test_progress_corrupt_file_starts_fresh(tmp_path):
path = tmp_path / ".progress.json"
path.write_text("not valid json")
p = ProgressTracker(path)
assert len(p) == 0 # starts fresh, no exception
# ---------------------------------------------------------------------------
# CsvWriter
# ---------------------------------------------------------------------------
FIELDS = ["a", "b", "c"]
def test_csv_writer_creates_header(tmp_path):
path = tmp_path / "out.csv"
w = CsvWriter(path, FIELDS)
w.close()
rows = list(csv.DictReader(path.open()))
assert rows == []
header = path.read_text().splitlines()[0]
assert header == "a,b,c"
def test_csv_writer_write_row(tmp_path):
path = tmp_path / "out.csv"
w = CsvWriter(path, FIELDS)
w.write({"a": "1", "b": "2", "c": "3"})
w.close()
rows = list(csv.DictReader(path.open()))
assert len(rows) == 1
assert rows[0]["a"] == "1"
assert rows[0]["c"] == "3"
def test_csv_writer_missing_fields_fill_empty(tmp_path):
path = tmp_path / "out.csv"
w = CsvWriter(path, FIELDS)
w.write({"a": "hello"}) # b and c missing
w.close()
rows = list(csv.DictReader(path.open()))
assert rows[0]["b"] == ""
assert rows[0]["c"] == ""
def test_csv_writer_appends_on_second_open(tmp_path):
path = tmp_path / "out.csv"
w = CsvWriter(path, FIELDS)
w.write({"a": "first"})
w.close()
w2 = CsvWriter(path, FIELDS)
w2.write({"a": "second"})
w2.close()
rows = list(csv.DictReader(path.open()))
assert len(rows) == 2
assert rows[0]["a"] == "first"
assert rows[1]["a"] == "second"
+149
View File
@@ -0,0 +1,149 @@
"""
Tests for spruce.recheck — synthetic archive tree under tmp_path.
These tests verify the key improvement: a single --recheck pass is enough.
Zero-byte tiles are deleted from disk AND their URLs removed from progress
without needing a second pass.
"""
from pathlib import Path
import pytest
from spruce.progress import ProgressTracker
from spruce.recheck import recheck_archive, recheck_tile_files
BASE_URL = "http://192.0.2.1:8010/index.php"
def _tile_url(scan_id: int, x: float, y: float) -> str:
return f"{BASE_URL}?cmd=image&mode=image_scan&id={scan_id}&s=1&x={x}&y={y}"
def _make_tile(path: Path, size: int = 1024) -> None:
"""Create a tile file. size=0 simulates a zero-byte / corrupt download."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"\xff" * size)
def _archive_tile_path(tmp_path: Path, scan_id: int, row: int, col: int) -> Path:
return (
tmp_path
/ "BW3-20__AMR-26_"
/ "2024-07-29"
/ str(scan_id)
/ "tiles"
/ f"tile_r{row:03d}_c{col:03d}.jpg"
)
# ---------------------------------------------------------------------------
# recheck_tile_files
# ---------------------------------------------------------------------------
def test_recheck_tile_files_no_zero_bytes(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
tile = _archive_tile_path(tmp_path, 158374, 0, 0)
_make_tile(tile, size=1024)
url = _tile_url(158374, 0.0, 0.0)
p.mark_done(url)
p.save()
deleted = recheck_tile_files(tmp_path, p)
assert deleted == 0
assert tile.exists()
assert p.is_done(url)
def test_recheck_tile_files_deletes_zero_byte(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
tile = _archive_tile_path(tmp_path, 158374, 0, 0)
_make_tile(tile, size=0)
url = _tile_url(158374, 0.0, 0.0)
p.mark_done(url)
p.save()
deleted = recheck_tile_files(tmp_path, p)
assert deleted == 1
assert not tile.exists()
def test_recheck_tile_files_single_pass_removes_url(tmp_path):
"""
The two-run wart is fixed: after recheck_tile_files the URL is already
removed from progress — no second pass required.
"""
p = ProgressTracker(tmp_path / ".progress.json")
tile = _archive_tile_path(tmp_path, 158374, 0, 0)
_make_tile(tile, size=0)
url = _tile_url(158374, 0.0, 0.0)
p.mark_done(url)
p.save()
recheck_tile_files(tmp_path, p)
# Reload progress from disk to confirm the change was persisted
p2 = ProgressTracker(tmp_path / ".progress.json")
assert not p2.is_done(url)
def test_recheck_tile_files_healthy_tiles_untouched(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
good = _archive_tile_path(tmp_path, 158374, 0, 0)
bad = _archive_tile_path(tmp_path, 158374, 0, 1)
_make_tile(good, size=512)
_make_tile(bad, size=0)
url_good = _tile_url(158374, 0.0, 0.0)
url_bad = _tile_url(158374, 3.01, 0.0)
p.mark_done(url_good)
p.mark_done(url_bad)
p.save()
deleted = recheck_tile_files(tmp_path, p)
assert deleted == 1
assert good.exists()
assert not bad.exists()
# ---------------------------------------------------------------------------
# recheck_archive
# ---------------------------------------------------------------------------
def test_recheck_archive_empty_progress(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
removed = recheck_archive(tmp_path, p)
assert removed == 0
def test_recheck_archive_healthy(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
tile = _archive_tile_path(tmp_path, 158374, 0, 0)
_make_tile(tile, size=1024)
p.mark_done(_tile_url(158374, 0.0, 0.0))
p.save()
removed = recheck_archive(tmp_path, p)
assert removed == 0
def test_recheck_archive_removes_missing_scan(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
# Mark a URL done but create no files on disk
p.mark_done(_tile_url(999999, 0.0, 0.0))
p.save()
removed = recheck_archive(tmp_path, p)
assert removed == 1
assert not p.is_done(_tile_url(999999, 0.0, 0.0))
def test_recheck_archive_skips_mosaic_urls(tmp_path):
p = ProgressTracker(tmp_path / ".progress.json")
mosaic_url = "http://192.0.2.1:8011/RootView_Database/158374/mosaic.jpg"
p.mark_done(mosaic_url)
p.save()
removed = recheck_archive(tmp_path, p)
assert removed == 0
assert p.is_done(mosaic_url) # mosaics are never touched
+91
View File
@@ -0,0 +1,91 @@
"""Tests for spruce.settings — config loading and worker clamping."""
import logging
import pytest
import yaml
from spruce.settings import (
MAX_SAFE_WORKERS,
_clamp_workers,
load_config,
)
# ---------------------------------------------------------------------------
# _clamp_workers
# ---------------------------------------------------------------------------
def test_clamp_workers_below_limit():
assert _clamp_workers(2) == 2
def test_clamp_workers_at_limit():
assert _clamp_workers(MAX_SAFE_WORKERS) == MAX_SAFE_WORKERS
def test_clamp_workers_above_limit_caps(caplog):
with caplog.at_level(logging.WARNING):
result = _clamp_workers(MAX_SAFE_WORKERS + 1)
assert result == MAX_SAFE_WORKERS
assert "exceeds the safe limit" in caplog.text
def test_clamp_workers_zero():
assert _clamp_workers(0) == 0
# ---------------------------------------------------------------------------
# load_config
# ---------------------------------------------------------------------------
def _write_config(tmp_path, **overrides):
base = {
"username": "testuser",
"password": "testpass",
}
base.update(overrides)
path = tmp_path / "config.yaml"
path.write_text(yaml.dump(base))
return str(path)
def test_load_config_defaults(tmp_path):
path = _write_config(tmp_path)
cfg = load_config(path)
assert cfg["base_url"] == "http://205.149.147.131:8010/"
assert cfg["workers"] == 2
assert cfg["timeout"] == 60
assert cfg["request_delay"] == 0.5
assert cfg["output_dir"] == "archives"
def test_load_config_overrides(tmp_path):
path = _write_config(tmp_path, workers=3, output_dir="my_archives")
cfg = load_config(path)
assert cfg["workers"] == 3
assert cfg["output_dir"] == "my_archives"
def test_load_config_caps_workers(tmp_path, caplog):
path = _write_config(tmp_path, workers=MAX_SAFE_WORKERS + 2)
with caplog.at_level(logging.WARNING):
cfg = load_config(path)
assert cfg["workers"] == MAX_SAFE_WORKERS
assert "exceeds the safe limit" in caplog.text
def test_load_config_missing_username_exits(tmp_path):
path = tmp_path / "config.yaml"
path.write_text(yaml.dump({"password": "x"}))
with pytest.raises(SystemExit):
load_config(str(path))
def test_load_config_missing_password_exits(tmp_path):
path = tmp_path / "config.yaml"
path.write_text(yaml.dump({"username": "x"}))
with pytest.raises(SystemExit):
load_config(str(path))