diff --git a/README.md b/README.md
index 468f046..0528c25 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1 @@
-backup-commander
-===============
-
-Simple web-based backup GUI
\ No newline at end of file
+Add this line in the sudoers file (use visudo) and it must go AFTER the %sudo line www-data ALL=(ALL) NOPASSWD: /usr/bin/php /var/www/html/bin/run-bu-by-id.php 3rd-party backup-commander dom_persist eclipse-gwt help_dlang help.txt html_backup_UI html_stanhope res_builder stanhope_d tmp Test by switching user to www-data: su -s /bin/bash www-data cd /var/www/html php bin/run-bu-by-id.php 19 0 where 19 is the backup id and 0/1 is not-test/test Comment out /inc/secure.php in post_handler.php if the login is not required TODO: create an install script: -- currently all manually done First version on Las-server Pre-requesite to push onto pearcey.net Generate a database schema with default login add the sudo entry for www-data (or other webserver user) kick off the schedule runner at the install stage create the schedule runner in php. simple polling of the DB get passwords from BD create a user credential page: new user+password change own password but only if the login is enabled (in post_handler.php)
diff --git a/html/bin/bu-n-err-18.txt b/html/bin/bu-n-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/bu-n-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/bu-n-err-19.txt b/html/bin/bu-n-err-19.txt
new file mode 100644
index 0000000..062fc5d
--- /dev/null
+++ b/html/bin/bu-n-err-19.txt
@@ -0,0 +1 @@
+ID: 19
diff --git a/html/bin/bu-n-op-19.txt b/html/bin/bu-n-op-19.txt
new file mode 100644
index 0000000..f566b18
--- /dev/null
+++ b/html/bin/bu-n-op-19.txt
@@ -0,0 +1,12 @@
+ID: 19
+sending incremental file list
+html/bin/
+html/bin/bu-n-err-19.txt
+html/bin/bu-n-op-19.txt
+html/bin/bu-x-op-19.txt
+html/bin/data.db
+html/js/
+html/js/main.js
+
+sent 33,631 bytes  received 127 bytes  67,516.00 bytes/sec
+total size is 226,992  speedup is 6.72
diff --git a/html/bin/bu-t-err-18.txt b/html/bin/bu-t-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/bu-t-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/bu-t-op-18.txt b/html/bin/bu-t-op-18.txt
new file mode 100644
index 0000000..8a74739
--- /dev/null
+++ b/html/bin/bu-t-op-18.txt
@@ -0,0 +1,51 @@
+/*!999999\- enable the sandbox mode */ 
+-- MariaDB dump 10.19  Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64)
+--
+-- Host: localhost    Database: johntest
+-- ------------------------------------------------------
+-- Server version	10.6.18-MariaDB-0ubuntu0.22.04.1
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `BULIST`
+--
+
+DROP TABLE IF EXISTS `BULIST`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `BULIST` (
+  `BUID` int(11) NOT NULL AUTO_INCREMENT,
+  `BUName` varchar(9) DEFAULT NULL,
+  PRIMARY KEY (`BUID`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `BULIST`
+--
+
+LOCK TABLES `BULIST` WRITE;
+/*!40000 ALTER TABLE `BULIST` DISABLE KEYS */;
+/*!40000 ALTER TABLE `BULIST` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2024-10-03 16:13:05
diff --git a/html/bin/bu-x-op-19.txt b/html/bin/bu-x-op-19.txt
new file mode 100644
index 0000000..9fcf8b5
--- /dev/null
+++ b/html/bin/bu-x-op-19.txt
@@ -0,0 +1 @@
+html/bin/rsync-n-op-19.txt
diff --git a/html/bin/data.db b/html/bin/data.db
new file mode 100644
index 0000000..e829a56
--- /dev/null
+++ b/html/bin/data.db
Binary files differ
diff --git a/html/bin/rsync-n-err-18.txt b/html/bin/rsync-n-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/rsync-n-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/rsync-n-err-19.txt b/html/bin/rsync-n-err-19.txt
new file mode 100644
index 0000000..062fc5d
--- /dev/null
+++ b/html/bin/rsync-n-err-19.txt
@@ -0,0 +1 @@
+ID: 19
diff --git a/html/bin/rsync-n-op-19.txt b/html/bin/rsync-n-op-19.txt
new file mode 100644
index 0000000..20ee8de
--- /dev/null
+++ b/html/bin/rsync-n-op-19.txt
@@ -0,0 +1,13 @@
+ID: 19
+sending incremental file list
+html/bin/
+html/bin/data.db
+html/bin/rsync-n-err-19.txt
+html/bin/rsync-t-err-19.txt
+html/bin/rsync-t-op-19.txt
+html/bin/rsync-x-op-19.txt
+html/inc/
+html/inc/bu-common.php
+
+sent 27,025 bytes  received 146 bytes  54,342.00 bytes/sec
+total size is 217,616  speedup is 8.01
diff --git a/html/bin/rsync-t-err-18.txt b/html/bin/rsync-t-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/rsync-t-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/rsync-t-err-19.txt b/html/bin/rsync-t-err-19.txt
new file mode 100644
index 0000000..062fc5d
--- /dev/null
+++ b/html/bin/rsync-t-err-19.txt
@@ -0,0 +1 @@
+ID: 19
diff --git a/html/bin/rsync-t-op-18.txt b/html/bin/rsync-t-op-18.txt
new file mode 100644
index 0000000..42f1c56
--- /dev/null
+++ b/html/bin/rsync-t-op-18.txt
@@ -0,0 +1,51 @@
+/*!999999\- enable the sandbox mode */ 
+-- MariaDB dump 10.19  Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64)
+--
+-- Host: localhost    Database: johntest
+-- ------------------------------------------------------
+-- Server version	10.6.18-MariaDB-0ubuntu0.22.04.1
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `BULIST`
+--
+
+DROP TABLE IF EXISTS `BULIST`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `BULIST` (
+  `BUID` int(11) NOT NULL AUTO_INCREMENT,
+  `BUName` varchar(9) DEFAULT NULL,
+  PRIMARY KEY (`BUID`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `BULIST`
+--
+
+LOCK TABLES `BULIST` WRITE;
+/*!40000 ALTER TABLE `BULIST` DISABLE KEYS */;
+/*!40000 ALTER TABLE `BULIST` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2024-10-03 15:59:07
diff --git a/html/bin/rsync-t-op-19.txt b/html/bin/rsync-t-op-19.txt
new file mode 100644
index 0000000..6f9197e
--- /dev/null
+++ b/html/bin/rsync-t-op-19.txt
@@ -0,0 +1,12 @@
+ID: 19
+sending incremental file list
+html/bin/
+html/bin/data.db
+html/bin/rsync-t-err-19.txt
+html/bin/rsync-t-op-19.txt
+html/bin/rsync-x-op-19.txt
+html/inc/
+html/inc/bu-common.php
+
+sent 1,238 bytes  received 43 bytes  2,562.00 bytes/sec
+total size is 217,366  speedup is 169.68 (DRY RUN)
diff --git a/html/bin/rsync-x-op-19.txt b/html/bin/rsync-x-op-19.txt
new file mode 100644
index 0000000..9fcf8b5
--- /dev/null
+++ b/html/bin/rsync-x-op-19.txt
@@ -0,0 +1 @@
+html/bin/rsync-n-op-19.txt
diff --git a/html/bin/run-bu-by-id.php b/html/bin/run-bu-by-id.php
new file mode 100644
index 0000000..7401295
--- /dev/null
+++ b/html/bin/run-bu-by-id.php
@@ -0,0 +1,25 @@
+<?php
+
+include_once __DIR__.'/../inc/bu-common.php';
+
+// NOTE:
+// php.ini requires register_argc_argv = On
+
+// $argv[0] => the name/path of this script
+$id = $argv[1];
+$isTest = $argv[2];
+
+$str_err = run_backup_block( $id, $isTest=='1' );
+
+/*
+ * Since this script is used from within the shell, we loose return values. Therefore, any error
+ * recieved from the previous call will be echoed to the console. It shall have the string
+ * "Error:" prepended.
+ * 
+ * The shell exec will pickup the echoed output and test for the "Error" string.
+ * 
+ * The output is silent if all went well.
+ */
+if($str_err!='') echo "Error: $str_err";
+
+?>
diff --git a/html/css/main.css b/html/css/main.css
new file mode 100644
index 0000000..9c6b9e2
--- /dev/null
+++ b/html/css/main.css
@@ -0,0 +1,188 @@
+* {
+    margin: 0px 0px 0px 0px;
+    padding: 0px 0px 0px 0px;
+  }
+
+  body, html {
+    padding: 3px 3px 3px 3px;
+    background-color: #D8DBE2;
+	 font-family: Ubuntu, Verdana, sans-serif;
+    font-size: 11pt;
+    text-align: center;
+  }
+
+  div.main_page {
+    position: relative;
+    display: table;
+
+    width: 1000px;
+
+    margin-bottom: 3px;
+    margin-left: auto;
+    margin-right: auto;
+    padding: 0px 0px 0px 0px;
+
+    border-width: 2px;
+    border-color: #212738;
+    border-style: solid;
+
+    background-color: #FFFFFF;
+
+    text-align: center;
+  }
+
+  div.content_section_text {
+    padding: 4px 8px 4px 8px;
+
+    color: #000000;
+    font-size: 100%;
+  }
+
+  div.content_section_text pre {
+    margin: 8px 0px 8px 0px;
+    padding: 8px 8px 8px 8px;
+
+    border-width: 1px;
+    border-style: dotted;
+    border-color: #000000;
+
+    background-color: #F5F6F7;
+
+    font-style: italic;
+  }
+
+  div.content_section_text p {
+    margin-bottom: 6px;
+  }
+
+  div.content_section_text ul, div.content_section_text li {
+    padding: 4px 8px 4px 16px;
+  }
+
+  div.section_header {
+    padding: 3px 6px 3px 6px;
+
+    background-color: #8E9CB2;
+
+    color: #FFFFFF;
+    font-weight: bold;
+    font-size: 112%;
+    text-align: center;
+    margin-top: 10px;
+  }
+
+  div.content_section_text a {
+    text-decoration: none;
+    font-weight: bold;
+  }
+
+
+  div.content_section_text a:link,
+  div.content_section_text a:visited,
+   div.content_section_text a:active {
+    background-color: #DCDFE6;
+    color: #0e3f2b;
+  }
+	
+  div.content_section_text a:hover {
+    background-color: #0e3f2b;
+    color: #DCDFE6;
+  }
+  
+.tblcenter {
+	width: 100%;
+	margin-left: 10px;
+	margin-right: 10px;
+	text-align: left;
+}
+	
+.flex_header{
+   display: flex;
+   justify-content: space-between;
+   align-items: stretch;
+   background-color: #0e3f2b;
+   border: solid lightgray 1px;
+   border-bottom: solid gray 1px;
+   min-height: 50px;
+   margin-bottom: 10px;
+}
+
+.shb_Title img {
+   margin: 3px 3px 3px 5px;
+}
+
+.flex_header .shb_Title {
+   display: flex;
+   flex-direction: row;
+   align-items: center;		
+   color: #cfa949;
+   font-size: 18px;
+   font-weight: bold;
+   font-family: Helvetica, Arial, sans-serif;
+   margin-left: 0px;
+}
+
+.flex_header .shb_TitleRHS {
+	display: flex;
+	flex-direction: row;
+	align-items: center; /*align-items is perpendicular to the flex-direction*/
+	color: #c9bda2;
+}
+
+.page_inner{
+	margin-left: 10px;
+	margin-right: 10px;	
+}
+
+.btn_mse:hover {
+	cursor: pointer;
+}
+
+.btn_exec{
+	background-color: #04AA6D;
+	border: none;
+	color: white;
+	padding: 5px;
+	text-align: center;
+	text-decoration: none;
+	display: inline-block;
+	font-size: 12px;	
+	border-radius: 8px;
+}
+.btn_small {
+	font-size: 10px;
+	border-radius: 6px;
+	padding: 2px 5px 2px 5px;
+}
+
+.td_label_bkup{
+	text-align: right;
+	padding-right: 15px;
+}
+
+table tr.high-row-style td {
+	padding-top: 15px;	
+}
+
+table tr td.td_icon {
+	width: 35px;
+}
+
+.two-col-panel {
+  padding-top: 15px;	
+  text-align: left;
+  display: grid;
+  grid-template-columns: 200px auto;  
+}
+
+.two-col-panel > div {  
+  margin-top: 5px;  
+}
+
+.two-col-panel input {  
+  width: 300px;  
+}
+
+.two-col-panel textarea {  
+  width: 300px;
+}
diff --git a/html/img/edit-icon-24x24.png b/html/img/edit-icon-24x24.png
new file mode 100644
index 0000000..7777001
--- /dev/null
+++ b/html/img/edit-icon-24x24.png
Binary files differ
diff --git a/html/img/edit-icon-32x32.png b/html/img/edit-icon-32x32.png
new file mode 100644
index 0000000..cc0151c
--- /dev/null
+++ b/html/img/edit-icon-32x32.png
Binary files differ
diff --git a/html/img/edit-icon-L.png b/html/img/edit-icon-L.png
new file mode 100644
index 0000000..b2ed893
--- /dev/null
+++ b/html/img/edit-icon-L.png
Binary files differ
diff --git a/html/img/green-tick.png b/html/img/green-tick.png
new file mode 100644
index 0000000..b86cdd8
--- /dev/null
+++ b/html/img/green-tick.png
Binary files differ
diff --git a/html/img/red-cross.png b/html/img/red-cross.png
new file mode 100644
index 0000000..b965b00
--- /dev/null
+++ b/html/img/red-cross.png
Binary files differ
diff --git a/html/img/trash-bin-red-24x24.jpg b/html/img/trash-bin-red-24x24.jpg
new file mode 100644
index 0000000..0dd4304
--- /dev/null
+++ b/html/img/trash-bin-red-24x24.jpg
Binary files differ
diff --git a/html/img/trash-bin-red-24x24.png b/html/img/trash-bin-red-24x24.png
new file mode 100644
index 0000000..bdec232
--- /dev/null
+++ b/html/img/trash-bin-red-24x24.png
Binary files differ
diff --git a/html/img/uk-green.png b/html/img/uk-green.png
new file mode 100644
index 0000000..dab2252
--- /dev/null
+++ b/html/img/uk-green.png
Binary files differ
diff --git a/html/inc/.htaccess b/html/inc/.htaccess
new file mode 100644
index 0000000..15f487c
--- /dev/null
+++ b/html/inc/.htaccess
@@ -0,0 +1,4 @@
+<Files myfile.txt>
+Order Allow,Deny
+Deny from all
+</Files>
diff --git a/html/inc/btn_run_bu.php b/html/inc/btn_run_bu.php
new file mode 100644
index 0000000..170ef10
--- /dev/null
+++ b/html/inc/btn_run_bu.php
@@ -0,0 +1,52 @@
+<?php
+
+include_once 'bu-common.php';
+
+
+function btn_run_bu( $buid, $testrun='true' ){
+	
+	//we cannot fork using php inside apache for some stupid reason.
+	//so we need to deligate a fork to an external process. 
+	//BASH can do this with the background '&' command, so we use it.
+	
+	$output = array();
+	$result_code = null;	
+	$bIsTest = $testrun=='true';
+	
+	if(!$bIsTest) $str_test = '0 &';
+	else $str_test = '1';
+
+	// get realpath, cos this is what is specified in visudo:
+	//www-data ALL=(ALL) NOPASSWD: /usr/bin/php /var/www/html/bin/run-bu-by-id.php *	
+	$path_to_script = realpath(__DIR__."/../bin/run-bu-by-id.php");
+	$command = "sudo php $path_to_script $buid $str_test";
+	
+	if( exec( $command, $output, $result_code )===FALSE){
+		error_log( "exec to php-cli failed - command: $command");
+		return array('error', "exec to php-cli failed - command: $command" );
+	}
+	//even though the script ran, it may have echoed errors to the console. We test here.
+	if( count($output)>0 ){
+		if( substr( $output[0], 0, 6) == 'Error:' ){
+			error_log( $output[0] );
+			return array('error', $output[0] );
+		}		
+	}	
+	
+	if($testrun!='false'){
+		//a test run waits for the command to finish, so the output can be collected and sent back now
+		for($i=0;$i<2;$i++){
+			$fn = __DIR__."/../bin/".getFilename_out( $buid, $bIsTest, $i==0 );
+			$output[] = file_get_contents($fn);
+		}
+
+		return array_merge( array('done'), $output );
+	}
+	
+	//error_log( "Backup kicked off for ID($buid): isTest=$isTest");
+	
+	return array_merge( array('running'), $output );
+}
+
+
+?>
diff --git a/html/inc/bu-common.php b/html/inc/bu-common.php
new file mode 100644
index 0000000..508fa44
--- /dev/null
+++ b/html/inc/bu-common.php
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * Retrieve a backup row from the database using it's ID.
+ * 
+ * Returns the following data as an array:
+ * 
+ * [ BUName, Dir_Src, Dir_Dest, Files_Ex, Bu_Type, BuRunning, BuError, LastRunDt ]
+ * 
+ */
+function get_bu_item( $id ){
+
+	$db = new SQLite3( __DIR__.'/../bin/data.db', SQLITE3_OPEN_READONLY );
+
+	$html_rows='';
+	$results = $db->query("SELECT * FROM BULIST where BUID=$id");
+	$row = $results->fetchArray();
+	
+	if($row){
+		return array( $row['BUName'], $row['Dir_Src'], $row['Dir_Dest'], $row['Files_Ex'], $row['BuType'], $row['BuRunning'], $row['BuError'], $row['LastRunDt'] );
+	}
+	
+}
+
+/**
+ * Set (unset) the running datbase flag. Return true on success. The flag is
+ * unaltered if running in test mode.
+ */
+function set_bu_running( $id, bool $setit, bool $bu_test ){
+	
+	if($bu_test) return true;
+
+	$db = new SQLite3( __DIR__.'/../bin/data.db' );
+	
+	$setit_int = $setit?1:0;
+	return $db->exec("update BULIST set BuRunning=$setit_int where BUID=$id");	
+}
+
+/**
+ * Set the error string in the database.
+ */
+function set_bu_error( $buid, $str_errs ){	
+	$db = new SQLite3( __DIR__.'/../bin/data.db' );
+	if($str_errs==''){
+		return $db->exec("update BULIST set BuError = NULL where BUID=$buid");	
+	}
+	$p = $db->prepare("update BULIST set BuError=:str_errs where BUID=:buid");	
+	$p->bindValue(':str_errs', $str_errs );
+	$p->bindValue(':buid', $buid );
+	$p->execute();
+		
+	if( $db->lastErrorCode()!==0){
+		return $db->lastErrorCode();
+	}
+}
+
+/**
+ * 
+ */
+function set_last_run_date( $buid, $str_dt ){
+	$db = new SQLite3( __DIR__.'/../bin/data.db' );
+	$p = $db->prepare("update BULIST set LastRunDt=:str_dt where BUID=:buid");	
+	$p->bindValue(':str_dt', $str_dt );
+	$p->bindValue(':buid', $buid );
+	$p->execute();
+		
+	if( $db->lastErrorCode()!==0){
+		return $db->lastErrorCode();
+	}
+}
+
+
+/**
+ * Run the backup process for the given backup ID.
+ * 
+ * Returns an error string on failure.
+ */
+function run_backup_block( $buid, $bu_test=true ){
+	
+	$row = get_bu_item( $buid );
+		
+	if( !$bu_test && $row[5]==1){
+		return "Backup for item($buid) is already running\n";		
+	}
+
+	$bu_name = $row[0];
+	$bu_src = $row[1];
+	$bu_dest = $row[2];
+	$exFiles = $row[3];
+	$bu_type = $row[4];
+	$bu_rnng = $row[5];
+
+	// NOTE:
+	// using pcntl_fork() was a waste of time because the parent always waited for the child to finish.
+	// so we rely on bash &
+	
+	if($bu_type==1 ){
+		return run_backup_DB( $bu_test, $buid, $bu_src, $bu_dest );	
+	}	
+
+	return run_backup_F( $bu_test, $buid, $bu_src, $bu_dest, $exFiles );
+}
+
+function run_backup_F( $bu_test, $buid, $bu_src, $bu_dest, $exFiles ){
+	
+	if(!set_bu_running( $buid, true, $bu_test )){
+		return "Could not set the DB flag";		
+	}
+
+	if($bu_test){
+		$flags = '-anv';	// n is the dry run indicator
+	}else{
+		$flags = '-av';
+		set_bu_error( $buid, '' );	//clear any previous error
+		set_last_run_date( $buid, gmdate("Y-m-d H:i:s") );
+	}
+
+	$fn_o = getFilename_out( $buid, $bu_test, false );
+	$fn_e = getFilename_out( $buid, $bu_test, true );
+
+	// create the output files with the ID in the first line
+	exec("echo 'ID: $buid' > bin/$fn_o");
+	exec("echo 'ID: $buid' > bin/$fn_e");		
+	
+	$str_exPaths = '';
+	if(trim( $exFiles )!=''){
+		$fn_x = getFilename_tmp( $buid, 'x' );
+		file_put_contents( "bin/$fn_x", $exFiles );
+		$str_exPaths = "--exclude-from=bin/$fn_x";
+	}
+
+	// bash command
+	//rsync -a --exclude-from=to-exclude.txt $PATH_SRC $BKUP_DEST
+	//rsync -anv --exclude-from=to-exclude.txt $PATH_SRC $BKUP_DEST	<-- for testing
+	$command = "rsync $flags $str_exPaths $bu_src $bu_dest 2>> bin/$fn_e 1>> bin/$fn_o";
+
+	$output = array();
+	$result_code = null;
+	if( exec( $command, $output, $result_code )===FALSE){		
+		set_bu_running( $buid, false, $bu_test );
+		return "rsync failed";
+	}	
+	set_bu_running( $buid, false, $bu_test );
+
+	// can we find any error strings in the error file?
+	$str_errs = file_get_contents( "bin/$fn_e" );
+	$pos = strpos($str_errs, 'error' );
+	if($pos===FALSE){
+		$pos = strpos($str_errs, 'failed' );
+	}
+	if($pos===FALSE) return;
+
+	//place it in the database
+	set_bu_error( $buid, $str_errs );
+	
+}
+
+
+/**
+ * Very similar to the file version. The common code is running the bach script.
+ */
+function run_backup_DB( $bu_test, $buid, $bu_src, $bu_dest ){
+
+	if(!set_bu_running( $buid, true, $bu_test )){
+		return "Could not set the DB flag";		
+	}
+	
+	if(!is_dir($bu_dest) ){
+		return "Destination ($bu_dest) is not a directory";
+	}
+
+	if($bu_test){
+		$fn_o = getFilename_out( $buid, $bu_test, false );		
+		$bu_dest = "bin";
+	}else{
+		$fn_o = "$bu_src.sql";		
+		set_bu_error( $buid, '' );	//clear any previous error
+		set_last_run_date( $buid, gmdate("Y-m-d H:i:s") );
+	}
+
+	// create the error files with the ID in the first line
+	$fn_e = getFilename_out( $buid, $bu_test, true );
+	exec("echo 'ID: $buid' > bin/$fn_e");
+
+	// bash command
+	//mysqldump johntest > /home/johnp/tmp/dbs/johntest.sql
+		
+	$command = "mysqldump $bu_src 2>> bin/$fn_e 1>> $bu_dest/$fn_o";
+
+	$output = array();
+	$result_code = null;
+	if( exec( $command, $output, $result_code )===FALSE){		
+		set_bu_running( $buid, false, $bu_test );
+		return "mysqldump failed";
+	}	
+	set_bu_running( $buid, false, $bu_test );
+
+	// can we find any error strings in the error file?
+	$str_errs = file_get_contents( "bin/$fn_e" );
+	$pos = strpos($str_errs, 'error' );
+	if($pos===FALSE){
+		$pos = strpos($str_errs, 'failed' );
+	}
+	if($pos===FALSE) return;
+
+	//place it in the database
+	set_bu_error( $buid, $str_errs );
+
+}
+
+function getFilename_out( $buid, bool $bu_test, bool $isErr=false ){	
+	return getFilename_tmp( $buid, $bu_test?'t':'n', $isErr? 'err': 'op' );	
+}
+
+/**
+ * Create an output file for the backup (forked) process to use. These are essentually
+ * temporary files which may be deleted after the back process completes.
+ * 
+ * buid:	backup record id
+ * 
+ * fn_type:
+ * 	t:	for test files
+ * 	n: for non-test files
+ * 	x: for file exclusion list
+ * 
+ * fn_stream:
+ * 	op: output stream
+ * 	err: error stream
+ */
+function getFilename_tmp( $buid, $fn_type='t', $fn_stream='op' ){	
+	return "bu-$fn_type-$fn_stream-$buid.txt";	
+}
+
+?>
diff --git a/html/inc/bu_list_content.php b/html/inc/bu_list_content.php
new file mode 100644
index 0000000..b5f9a2e
--- /dev/null
+++ b/html/inc/bu_list_content.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * Provides a simple json array of the BU state info for the UI to poll changes.
+ */
+function getbu_list_state(){
+
+	$db = new SQLite3('bin/data.db', SQLITE3_OPEN_READONLY );
+
+	$bu_state = array();	
+	$results = $db->query('SELECT BUID, BuRunning FROM BULIST');
+	while ($row = $results->fetchArray()) {
+		array_push( $bu_state, array( $row['BUID'], $row['BuRunning'] ) );
+	}
+	return $bu_state;
+}
+
+
+/**
+ * Returns the main backup list page.
+ */
+function getbu_list_content(){
+	
+	$db = new SQLite3('bin/data.db', SQLITE3_OPEN_READONLY );
+
+	$html_rows='';
+	$results = $db->query('SELECT * FROM BULIST');
+	while ($row = $results->fetchArray()) {
+		 //var_dump($row);
+
+		$view_type = 'edit-file-bu';
+		if($row['BuType']==1){
+			$view_type = 'edit-db-bu';
+		}
+		
+		$html_row = '<tr>';
+
+		$html_row .= '<td>'.$row['BUID'].'</td>';
+		$html_row .= "<td class='td_icon' title='edit' ><img class=\"btn_mse\" onclick=\"btn_clk_nav(this, '$view_type', {$row['BUID']})\" src='img/edit-icon-24x24.png'></td>";		// edit icon
+		$html_row .= "<td class='td_icon' title='delete' ><img class=\"btn_mse\" onclick=\"btn_clk_nav(this, 'delete-bu', {$row['BUID']})\" src='img/trash-bin-red-24x24.png'></td>";	// trash icon
+		$html_row .= "<td class=\"btn_mse\" onclick=\"btn_clk_nav(this, '$view_type', {$row['BUID']})\" >{$row['BUName']}</td>";
+		$but = $row['BuType']==1?'DB':'F';
+		$html_row .= "<td>$but</td>";
+		
+		if($row['BuError']!=''){		
+			$html_row .= "<td><span style='color:red;'>!! </span>{$row['LastRunDt']}</td>";
+		}else{
+			$html_row .= "<td>{$row['LastRunDt']}</td>";
+		}
+		
+		if( $row['BU_REPEAT']=='N' ){
+			$html_row .= "<td title='edit schedule' ><img src=\"img/red-cross.png\" class=\" btn_mse\" onclick=\"btn_clk_nav(this, 'sched', {$row['BUID']})\" ></td>";
+		}else{
+			$html_row .= "<td title='edit schedule' ><button class=\"btn_mse btn_exec btn_small\" onclick=\"btn_clk_nav(this, 'sched', {$row['BUID']})\" >Edit</button> {$row['BU_REPEAT']}</td>";
+		}
+
+		if($row['BuRunning']){
+			$html_row .= '<td><img src="img/green-tick.png"></td>';
+		}else{
+			$html_row .= "<td id=\"td_run_{$row['BUID']}\" ><button class=\"btn_mse btn_exec btn_small\" onclick=\"btn_clk_nav(this, 'btn_run_bu noTest', {$row['BUID']})\" >Run Now</button></td>";
+		}
+		
+		$html_row .= '</tr>';
+	
+		$html_rows .= $html_row;
+	}
+
+$content = <<<'EOD'
+
+	<div class="section_header" >Backup Config List</div>
+		
+	<div class="content_section_text" style="min-height: 350px;" >
+		<br>
+		<table class="tblcenter" >
+			
+			<tr>
+				<th>ID</th>
+				<th></th>
+				<th></th>
+				<th>Name</th>
+				<th>Type</th>
+				<th>Last Run (UTC)</th>
+				<th>Scheduled</th>
+				<th>Running</th>				
+			</tr>
+			--INSERT-ROWS--			
+			<tr class="high-row-style">
+				<td></td>
+				<td></td>
+				<td></td>
+				<td class="td_label_bkup" >Database backup</td>
+				<td> <button class="btn_mse btn_exec" onclick="btn_clk_nav(this, 'new-db-bu')">Create</button> </td>
+			</tr>
+			
+			<tr>
+				<td></td>
+				<td></td>
+				<td></td>
+				<td class="td_label_bkup" >Files backup</td>
+				<td> <button class="btn_mse btn_exec" onclick="btn_clk_nav(this, 'new-file-bu' )">Create</button> </td>
+			</tr>
+			
+		</table>
+	</div>
+EOD;
+
+	return str_replace('--INSERT-ROWS--', $html_rows, $content);
+
+}
+
+?>
diff --git a/html/inc/common.php b/html/inc/common.php
new file mode 100644
index 0000000..240ad08
--- /dev/null
+++ b/html/inc/common.php
@@ -0,0 +1,352 @@
+<?php
+/**
+ * COPYRIGHT © 2022 JOHN PEARCEY
+ * All rights reserved
+*/
+
+const BASE64_DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$_';
+
+if (version_compare( PHP_VERSION, '8', '<') ){	
+	include_once __DIR__ . "/common-pre-v8.php";
+}
+
+function getStackTrace(){
+	try{
+		throw new Exception;
+	}catch( Exception $e ){
+		return $e->getTraceAsString();
+	}
+}
+
+/**
+ * e.g. split 
+ * 	../db/stanhopetest/img/img_52548
+ * into 
+ * [ ../db/stanhopetest/img/ , img_52548 ]
+ * 
+ */
+function split_file_path( $ffn ){
+	$fn = basename( $ffn ); 				// img_52548
+	//$fp = substr( $ffn, 0, strlen($ffn) - strlen($fn) );
+	$fp = dirname( $ffn ) . '/';
+	return [ $fp, $fn ];
+}
+
+/**
+ * Returns true if it is possible to create this directory on the file system.
+ * Includes the possibility of multiple directories.
+ */
+function is_creatable_dir( $dirs ){
+	$dir = $dirs; 
+	while(!file_exists($dir)){
+		$dir = dirname( $dir ); 
+	}
+	return is_writable($dir);
+}
+
+/**
+ * Function to make directories and actually SET the permissions
+ * https://www.php.net/manual/en/function.chmod.php
+ * 
+ * $perms is an octal number
+ * 	e.g. mkdirs( $dir, 02770 ); //770 and g+s
+ */
+function mkdirs( $dirs, $perms ){
+	$dir = $dirs; 
+	while(!file_exists($dir)){
+		$dir = dirname( $dir ); 
+	}
+	$rootDir = $dir;
+	if(!file_exists($dirs)) mkdir( $dirs, 0750, true );	//perms not always set
+	$dir = $dirs; 
+	while( $dir!=$rootDir) {
+		chmod( $dir, $perms );
+		$dir = dirname( $dir ); 
+	}
+}
+
+/**
+ * Return the contents of a url as a string using the curl functions.
+ */
+function getContentsFromUrl( $url ){
+	
+	$ch = curl_init( $url );
+	//$fp = fopen("example_homepage.txt", "w");
+	$fp = tmpfile();
+	
+	curl_setopt($ch, CURLOPT_FILE, $fp);
+	curl_setopt($ch, CURLOPT_HEADER, 0);
+
+	curl_exec($ch);
+	$c_err = curl_error($ch);
+	curl_close($ch);
+	if($c_err) {
+		error_log( $c_err );
+		fclose($fp);
+		return;
+	}
+
+	$f_path = stream_get_meta_data($fp)['uri'];
+	$rtn = file_get_contents( $f_path );
+	fclose($fp);
+
+	return $rtn;
+}
+
+/**
+ * Set the given key-value pair.
+ * 
+ * Returns the record ID.
+ */
+function params_set( $key, $val ) {
+
+	$rec = R::findOne( 'params', 'pkey=?', [$key] );
+	if($rec) {
+		$rec->pval = $val;
+		R::store( $rec );
+		return $rec->id;
+	}
+
+	$rec = R::dispense( 'params' );
+	$rec->pkey = $key;
+	$rec->pval = $val;
+	return R::store( $rec );
+}
+
+function params_get_date( $key ){
+	return strtotime( params_get($key) );
+}
+
+/**
+ * Returns the value for the given key. Returns null if no such key exists.
+ */
+function params_get( $key ){
+
+	$rec = R::findOne( 'params', 'pkey=?', [$key] );
+	if(!$rec) return null;
+	return $rec->pval;
+}
+
+/**
+ * A novel way of creating a unique ID each time a call is made. The new ID is
+ * returned.
+ * 
+ * e.g.
+ * This can be used to give a client which might want to create temporary unique IDs. It is
+ * called once per client refresh and used to ensure uniqueness of certain CSS data points.
+ */
+function params_unique( ) {
+
+	$rec = R::dispense( 'params' );
+	$rec->pkey = 'unique';
+	$id = R::store( $rec );	
+	R::exec( "delete from params where id<$id and pkey='unique'" );
+	return $id;
+}
+
+/**
+ * Return a string representation for the given dom node suitable for debugging.
+ */
+function getNodeString( $rmElem ){
+	
+	if($rmElem==null) return "Element is null";
+	$str="";
+	if( $rmElem->nodeType==XML_ELEMENT_NODE){
+		$str .= $rmElem->nodeName;
+		$a = $rmElem->getAttribute("shb_compid");
+		if($a){
+			$str .= " shb_compid=$a";
+		}
+	}else{
+		$str .= 'type = '.$rmElem->nodeType;
+	}
+	return $str;
+}
+
+/**
+ * Simple logger to write to a file. Using php error_log distorts all crlf pairs. This
+ * is a pain when you're trying to preserve them in html.
+ */
+function mylog( $msg ){
+	
+	global $logging_on;
+	
+	if(!$logging_on) return;
+	$p = __DIR__ . "/../../shb.log";
+	$h = fopen( $p, "a");  
+	if($h===false) return;
+	fwrite( $h, "$msg\n");
+	fclose($h);
+
+}
+
+function generateNonce( $digitCount = 12 ){
+	// e.g. 422466a05f24b09c978fa1f6
+	return bin2hex( random_bytes($digitCount) );
+}
+
+/**
+ * Generate a randon username starting with a letter and containing only letters and numbers.
+ */
+function generateRndUserName( $digitCount = 10 ){
+	$l1 = generateRndStrUsing( 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 1 );
+	return $l1 . generateRndStrUsing( BASE64_DIGITS, $digitCount-1 );
+}
+
+/**
+ * Generate a randon password with letters and numbers and some funny stuff.
+ */
+function generateRndPwd( $digitCount = 10 ){	
+	return generateRndStrUsing( '0123456789_!"£$%^&*()_-|\?/<>#~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', $digitCount );	
+}
+
+/**
+ * Generate a randon string of asc char. Default consisting of only [0-9][a-z][A-Z]
+ */
+function generateRndStrUsing( $using=BASE64_DIGITS, $digitCount = 8 ){
+	
+	$str = '';
+	$maxlen = strlen($using);
+	for($i=0; $i<$digitCount; $i++){
+		$iv = random_int(0, $maxlen-1);
+		$str .= mb_substr( $using, $iv, 1);	//use mb_substr because £ is not and asc char!
+	}
+	return $str;
+}
+
+
+/**
+ * Remove all html from the given string returning just the text content.
+ */
+function removeHtml( $str ){
+	//txt =	txt.replace(/<div>/gi, '\n');	
+	return preg_replace( '/<\/?[^>]+>/i', '', $str );
+}
+
+function getHtmlOkAsString( $data ){
+	$data = json_encode( $data );
+	return "[\"OK\", $data ]";		
+}
+	
+function sendHtmlOk( $data="" ){
+	echo getHtmlOkAsString( $data );
+}
+
+/**
+ * An effort to rectify my previously ill-thought out return encoding. You do not
+ * need to encode the data before calling this method. Any arrays will be json encoded.
+ */
+function sendHtmlOk_WithData( $data ){
+	$data = json_encode( $data );
+	echo "[\"OK\", $data ]";	
+}
+
+function getHtmlErrorAsString( $strErrDesc = "--blank--" ){
+	$data = json_encode( $strErrDesc );
+	return "[\"Error\", $data]";
+}
+
+function sendHtmlError( $strErrDesc = "--blank--" ){
+	$rtn = getHtmlErrorAsString( $strErrDesc );
+	mylog( $rtn );
+	echo $rtn;
+}
+
+/**
+ * 
+ */
+function getAttForPath( $name, $prefPath, $path){
+	return "$name = \"$prefPath/$path\"";
+}
+
+global $g_pub_path;
+
+/**
+ * If the browser=true, the file returned will be correct for adding to an href or src attribute so that the
+ * browser will find the link.
+ */
+function getDirForType( $filetype='html', $browser=false, $userid=-1 ){
+	
+	global $g_pub_path;
+		
+	if($g_pub_path==null){
+		$g_pub_path = params_get('pub_path');
+		if($g_pub_path==null){
+			throw new Exception("Publishing now requires a valid key('pub_path') in the params table for the publishing output path");
+		}
+		
+		if(str_ends_with( $g_pub_path, '/') ) $g_pub_path = substr($g_pub_path, 0, strlen($g_pub_path)-1 );
+		
+		//ensure all the directories exist
+		createUserDir( 'img', $userid );
+		createUserDir( 'css', $userid );
+		createUserDir( 'html', $userid );
+		createUserDir( 'js', $userid );
+		
+	}
+
+	return getDirForType_( $filetype, $browser, $userid );
+
+}
+
+function createUserDir( $dName, $userid ){
+	
+	if(!file_exists(getDirForType_( $dName, false, $userid ))) {
+		$dir = getDirForType_( $dName, false, $userid );
+		error_log("common.php: createUserDir: $dir");
+		mkdir( $dir, 0770, true);	
+	}
+}
+
+/**
+ * Part of the new path handling system is to sepatate out path names from real files
+*/
+function mkdirIfNotExists( $d ){
+	if(!file_exists( $d ) ) {
+		error_log("common.php: createUserDir: $d");
+		mkdir( $d, 0770, true );	
+	}
+}
+
+/**
+ * For the moment, call getDirForType and not this function. In time, the setup routine will make the directories
+ * and eliminate the need for the check during a user session.
+ * 
+ * If the browser flag is set, we assuming a document root sub-dir. But in time, this will need to also cater for
+ * a site serving the g_pub_path as root (wrt the browser).
+ * 
+ */
+function getDirForType_( $filetype, $browser, $userid ){	
+	
+	global $g_pub_path;
+	$strPath = $g_pub_path;	
+	
+	
+	if($userid>0){
+		$strPath .= "_dbg_$userid";		
+	}
+	
+	switch($filetype){
+	
+	case 'img':
+		if($browser) return "/$strPath/img/";	
+		return fxdPth( "$strPath/img/" );		
+
+	case 'js':
+		if($browser) return "/$strPath/js/";	
+		return fxdPth( "$strPath/js/" );		
+
+	case 'css':
+		if($browser) return "/$strPath/css/";
+		return fxdPth( "$strPath/css/" );		
+					
+	case 'html':
+	default:
+		if($browser) return "/$strPath/";
+		return fxdPth( "$strPath/" );
+		
+	}	
+}
+
+
+//NO white-space
diff --git a/html/inc/delete-file-bu.php b/html/inc/delete-file-bu.php
new file mode 100644
index 0000000..b24d99c
--- /dev/null
+++ b/html/inc/delete-file-bu.php
@@ -0,0 +1,17 @@
+<?php
+
+
+function delete_bu_item( $id ){
+			
+	$db = new SQLite3('bin/data.db' );
+	$db_st = $db->prepare('delete from BULIST where BUID=:buid');	
+	$db_st->bindValue(':buid', $id );	
+	$db_st->execute();
+		
+	if( $db->lastErrorCode()!==0){
+		return $db->lastErrorCode();
+	}
+
+}
+
+?>
diff --git a/html/inc/edit-sh-bu.php b/html/inc/edit-sh-bu.php
new file mode 100644
index 0000000..920a1c7
--- /dev/null
+++ b/html/inc/edit-sh-bu.php
@@ -0,0 +1,111 @@
+<?php
+
+function edit_sh_bu_getContent( $db_id ){
+	
+	$db = new SQLite3('bin/data.db', SQLITE3_OPEN_READONLY );
+
+	$db_st = $db->prepare('SELECT * FROM BULIST where BUID=:buid');
+	$db_st->bindValue(':buid', $db_id);
+	$results = $db_st->execute();
+	$row = $results->fetchArray();
+	if ( $row===FALSE ) {
+		 return "Item $db_id not found";
+	}
+	
+	$bun = $row['BUName'];
+	$bus = $row['Dir_Src'];
+	$bu_dt = $row['BU_DATE'];
+	if($bu_dt==''){
+		$bu_dt = date("Y-m-d");
+	}
+	$bu_t = $row['BU_TIME'];
+	if($bu_t==''){
+		$bu_t = '00:00';
+	}
+	$bu_r = $row['BU_REPEAT'];
+
+	switch($bu_r){
+	case 'D':
+		$id_checked = 'id="sched_ip_daily"';
+		break;
+	case 'W':
+		$id_checked = 'id="sched_ip_weekly"';
+		break;
+	case 'M':
+		$id_checked = 'id="sched_ip_monthly"';
+		break;
+	case 'N':
+	default:
+		$id_checked = 'id="sched_ip_none"';
+	}
+	
+$content = <<<'EOD'
+
+	<div class="section_header" >Edit Backup Schedule</div>
+		
+	<div class="content_section_text" style="min-height: 350px;" >
+	
+		<div class="two-col-panel">
+			
+			<div>ID</div>
+			<div>$db_id</div>
+			
+			<div>Backup Name</div>
+			<div>$bun</div>
+			
+			<div>Data Source</div>
+			<div>$bus</div>
+
+			<div>Next Run Date/Time</div>
+			<div><input id="sched_date" type="date" value="$bu_dt" ></div>
+			
+			<div></div>
+			<div><input id="sched_time" type="time" value="$bu_t"></div>
+
+			<div>Repeat</div>
+			<div>
+				<input style="width: 25px;" type="radio" name="sched_period" id="sched_ip_none" ><label for="sched_ip_none">None</label><br>
+				<input style="width: 25px;" type="radio" name="sched_period" id="sched_ip_daily"><label for="sched_ip_daily">Daily</label><br>
+				<input style="width: 25px;" type="radio" name="sched_period" id="sched_ip_weekly" ><label for="sched_ip_weekly">Weekly</label><br>
+				<input style="width: 25px;" type="radio" name="sched_period" id="sched_ip_monthly"><label for="sched_ip_monthly">Monthly</label>
+			</div>
+
+			<div></div>			
+			<div>				
+				<button class="btn_exec" onclick="btn_clk_nav(this, 'btn_edit_shed_bu', $db_id)" >Save</button>
+				<button class="btn_exec" onclick="btn_clk_nav(this, 'btn_cancel')" >Cancel</button>
+			</div>			
+
+		</div>
+
+	</div>
+EOD;
+	
+	$content = str_replace($id_checked, $id_checked.' checked ', $content );
+	$content = str_replace('$bus', $bus, $content);
+	$content = str_replace('$bun', $bun, $content);
+	$content = str_replace('$bu_dt', $bu_dt, $content);
+	$content = str_replace('$bu_t', $bu_t, $content);
+	return str_replace('$db_id', $db_id, $content);
+}
+
+function save_bu_schedule( $postVars ){
+		
+	//error_log( $postVars['id'] );
+		
+	$db = new SQLite3('bin/data.db' );
+
+	$db_st = $db->prepare('UPDATE BULIST SET BU_DATE=:bud, BU_TIME=:but, BU_REPEAT=:bur where BUID=:buid');	
+	$db_st->bindValue(':buid', $postVars['id'], SQLITE3_INTEGER );
+	$db_st->bindValue(':bud', $postVars['sched_date'] );
+	$db_st->bindValue(':but', $postVars['sched_time'] );
+	$db_st->bindValue(':bur', $postVars['sched_period'] );		
+	$db_st->execute();
+		
+	if( $db->lastErrorCode()!==0){
+		return $db->lastErrorCode();
+	}
+
+}
+
+?>
diff --git a/html/inc/login.php b/html/inc/login.php
new file mode 100644
index 0000000..9304f5c
--- /dev/null
+++ b/html/inc/login.php
@@ -0,0 +1,45 @@
+<?php
+
+
+function getLogin_content( ){
+	
+$content = <<<'EOD'
+
+	<div class="section_header" >Login</div>
+		
+	<div class="content_section_text" style="min-height: 350px;" >
+	
+		<div class="two-col-panel">
+						
+			<div>Login Name</div>
+			<div><input id="login_name" type="text" $login_name_value ></div>
+			
+			<div>Password</div>
+			<div><input id="login_pwd" type="password" ></div>
+
+
+			<div></div>			
+			<div>
+				<button class="btn_exec" onclick="btn_clk_nav(this, 'btn_login')" >Login</button>								
+			</div>
+
+		</div>
+
+	</div>
+EOD;
+		
+	//$content = str_replace('$header_cont', $header_cont, $content);
+	return $content;
+}
+
+function authenticate( $username, $hashed_pwd_client, $svr_nonce ){
+	
+	//TODO:
+	// get the username and password from somewhere.
+	$hashed_pwd_correct = hash('sha256', 'biggles'.$svr_nonce );
+	
+	return $hashed_pwd_correct == $hashed_pwd_client;
+}
+
+
+?>
diff --git a/html/inc/new-file-bu.php b/html/inc/new-file-bu.php
new file mode 100644
index 0000000..0c6951d
--- /dev/null
+++ b/html/inc/new-file-bu.php
@@ -0,0 +1,172 @@
+<?php
+
+include_once 'bu-common.php';
+
+
+/**
+ * This page provides the UI for a backup item for both new items and editing items for both 
+ * database style and files. So there are 4 combinations:
+ * 
+ * $bDbStyle = true, $db_id = 0:		Create new database backup item
+ * $bDbStyle = false, $db_id = 0:	Create new files backup item
+ * $bDbStyle = true, $db_id != 0:	Edit an existing database backup item
+ * $bDbStyle = false, $db_id != 0:	Edit an existing files backup item
+ * 
+ */
+function getbu_cf_content( $bDbStyle = false, $db_id = 0 ){
+	
+	$id_line='';
+	$header_cont='';
+	$lab1_cont = '';
+	$exfiles_cont = '';
+	$btn_new_bu = '';	
+	$item_data = null;
+	
+	$bu_name_value = '';
+	$bu_src_value = '';
+	$bu_dest_value = '';
+	$bu_ex_files = '';
+	$lastRunTime = '';
+	$bu_error_list = '';
+	
+	if($db_id>0){
+		$id_line = "<div>ID</div><div>$db_id</div>";
+		$item_data = get_bu_item( $db_id ); // [ BUName, Dir_Src, Dir_Dest, Files_Ex, Bu_Type, BuRunning, BuError, LastRunDt ]
+		
+		$bu_name_value = "value='{$item_data[0]}'";
+		$bu_src_value = "value='{$item_data[1]}'";
+		$bu_dest_value = "value='{$item_data[2]}'";	
+		$bu_ex_files = $item_data[3];		
+		
+		$bu_error_list = str_replace( "\n", '<br>', $item_data[6]);				
+		$bu_error_list = "<div>Errors</div><div>$bu_error_list</div>";
+			
+		$lastRunTime = "<div>Last Run (UTC)</div><div>{$item_data[7]}</div>";
+		
+	}
+	
+	if($bDbStyle){
+		if($db_id>0){			
+			$header_cont = '<div class="section_header" >Edit Database Backup Item</div>';
+			$btn_new_bu = "btn_edit_bu_db','$db_id";
+			
+		}else{
+			$header_cont = '<div class="section_header" >Create New Database Backup Item</div>';
+			$btn_new_bu = 'btn_new_bu_db';
+			
+		}
+		$lab1_cont = '<div>Database name</div>';
+		
+	}else{
+		if($db_id>0){
+			$header_cont = '<div class="section_header" >Edit File Backup Item</div>';
+			$btn_new_bu = "btn_edit_bu_file','$db_id";
+						
+		}else{
+			$header_cont = '<div class="section_header" >Create New File Backup Item</div>';
+			$btn_new_bu = 'btn_new_bu_file';
+			
+		}
+		
+		$lab1_cont = '<div>Source File/Dir</div>';
+		$exfiles_cont = "<div>Exclude Files</div><div><textarea id=\"new_bu_exf\" rows=\"4\" >$bu_ex_files</textarea></div>";
+	}
+	
+$content = <<<'EOD'
+
+	$header_cont
+		
+	<div class="content_section_text" style="min-height: 350px;" >
+	
+		<div class="two-col-panel">
+						
+			$id_line
+			
+			<div>Backup Name</div>
+			<div><input id="new_bu_name" type="text" $bu_name_value > (optional)</div>
+			
+			$lab1_cont
+			<div><input id="new_bu_source" type="text" $bu_src_value ></div>
+
+			<div>Destination Dir</div>
+			<div><input id="new_bu_dest" type="text" $bu_dest_value ></div>
+
+			$exfiles_cont
+
+			<div></div>			
+			<div>
+				<button class="btn_exec" onclick="btn_clk_nav(this, 'btn_run_bu')" >Test</button>
+				<button class="btn_exec" onclick="btn_clk_nav(this, '$btn_new_bu')" >Save</button>
+				<button class="btn_exec" onclick="btn_clk_nav(this, 'btn_cancel_new_bu')" >Cancel</button>
+			</div>
+
+			<div>Test output</div>
+			<div><textarea id="ta_testoutput" rows="8" readonly ></textarea></div>
+
+			$lastRunTime
+			$bu_error_list
+
+		</div>
+
+	</div>
+EOD;
+		
+	$content = str_replace('$bu_error_list', $bu_error_list, $content);
+	$content = str_replace('$lastRunTime', $lastRunTime, $content);
+	$content = str_replace('btn_run_bu\'', "btn_run_bu', $db_id", $content);
+	$content = str_replace('$bu_name_value', $bu_name_value, $content);
+	$content = str_replace('$bu_src_value', $bu_src_value, $content);
+	$content = str_replace('$bu_dest_value', $bu_dest_value, $content);
+		
+	$content = str_replace('$id_line', $id_line, $content);
+	$content = str_replace('$btn_new_bu', $btn_new_bu, $content);
+	$content = str_replace('$exfiles_cont', $exfiles_cont, $content);
+	$content = str_replace('$lab1_cont', $lab1_cont, $content);
+	$content = str_replace('$header_cont', $header_cont, $content);
+	return $content;
+}
+
+/**
+ * Both inserts as well as edits a single backup item.
+ */
+function save_bu_item( $postVars, $bu_id=0 ){
+		
+	//error_log( "save_bu_item id = $bu_id, new_bu_dest =  {$postVars['new_bu_dest']}" );	
+		
+	$db = new SQLite3('bin/data.db' );
+	$db_st = null;
+	if( $postVars['new_bu_is_dbType'] ){
+		if($bu_id==0){
+			$db_st = $db->prepare('INSERT INTO BULIST ( BUName, Dir_Src, Dir_Dest, BuType ) values( :bun, :bus, :bud, 1 )');
+		}else{
+			$db_st = $db->prepare('update BULIST set BUName=:bun, Dir_Src=:bus, Dir_Dest=:bud where BUID=:buid');
+		}
+	}else{
+		if($bu_id==0){
+			$db_st = $db->prepare('INSERT INTO BULIST ( BUName, Dir_Src, Dir_Dest, Files_Ex, BuType ) values( :bun, :bus, :bud, :buex, 0 )');
+		}else{
+			$db_st = $db->prepare('update BULIST set BUName=:bun, Dir_Src=:bus, Dir_Dest=:bud, Files_Ex=:buex where BUID=:buid');
+		}
+	}	
+	
+	if($bu_id>0){
+		$db_st->bindValue(':buid', $bu_id );
+	}
+	
+	$db_st->bindValue(':bun', $postVars['new_bu_name'] );
+	$db_st->bindValue(':bus', $postVars['new_bu_source'] );
+	$db_st->bindValue(':bud', $postVars['new_bu_dest'] );		
+	if( !$postVars['new_bu_is_dbType'] ){		
+		$db_st->bindValue(':buex', $postVars['new_bu_exf'] );		
+	}
+	
+	$db_st->execute();
+		
+	if( $db->lastErrorCode()!==0){
+		return $db->lastErrorCode();
+	}
+
+}
+
+
+?>
diff --git a/html/inc/pagemap.php b/html/inc/pagemap.php
new file mode 100644
index 0000000..11801de
--- /dev/null
+++ b/html/inc/pagemap.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * COPYRIGHT © 2024 JOHN PEARCEY
+ * All rights reserved
+*/
+
+
+include_once __DIR__ . "/common.php";
+
+/**
+ * The commands should all return true to indicate that they are valid calls from the client.
+ */
+function invokeCommand( $cmd, $postvars ){
+	
+	if( $cmd=='backup-list-state' ){	
+		include_once __DIR__ . "/bu_list_content.php";		
+		sendHtmlOk_WithData( [ 'state', getbu_list_state() ] );
+		return true;
+	}
+
+	if( $cmd=='backup-list' ){	
+		include_once __DIR__ . "/bu_list_content.php";		
+		sendHtmlOk_WithData( [ 'page', getbu_list_content() ] );
+		return true;
+	}
+
+	if( $cmd=='edit-db-bu' ){	
+		include_once __DIR__ . "/new-file-bu.php";
+		sendHtmlOk_WithData( [ 'page', getbu_cf_content( true, $postvars['id'] ) ] );
+		return true;
+	}
+
+	if( $cmd=='edit-file-bu' ){
+		include_once __DIR__ . "/new-file-bu.php";
+		sendHtmlOk_WithData( [ 'page', getbu_cf_content( false, $postvars['id'] ) ] );
+		return true;
+	}
+
+	if( $cmd=='new-db-bu' ){
+		include_once __DIR__ . "/new-file-bu.php";
+		sendHtmlOk_WithData( [ 'page', getbu_cf_content(true) ] );
+		return true;
+	}
+
+	if( $cmd=='new-file-bu' ){		
+		include_once __DIR__ . "/new-file-bu.php";
+		sendHtmlOk_WithData( [ 'page', getbu_cf_content() ] );
+		return true;
+	}
+
+	if( $cmd=='sched' ){		 
+		include_once __DIR__ . "/edit-sh-bu.php";		
+		sendHtmlOk_WithData( [ 'page', edit_sh_bu_getContent( $postvars['id'] ) ] );
+		return true;
+	}
+
+	if( $cmd=='btn_run_bu' ){
+		
+		include_once __DIR__ . "/btn_run_bu.php";				
+		$res = btn_run_bu( $postvars['id'], $postvars['isTest'] );
+		
+		if($res[0]=='running'){
+			// script kicked off asynchronously
+			sendHtmlOk_WithData( $res );
+			return true;
+		}
+		
+		if($res[0]=='done'){
+			// script ran synchronously and finished			
+			sendHtmlOk_WithData( $res );
+			return true;
+		}
+
+		if($res[0]=='error'){
+			sendHtmlError( $res );
+			return true;
+		}
+
+		sendHtmlError( 'Unknown return value from btn_run_bu' );
+		return true;
+	}
+
+	if( $cmd=='edit_shed_bu' ){
+		include_once __DIR__ . "/edit-sh-bu.php";
+		$db_err = save_bu_schedule( $postvars );	
+		if($db_err>0){
+			sendHtmlError( "DB error: $db_err" );
+			return true;
+		}
+		
+		if($postvars['final-page'] == '' ){
+			error_log( "Error: Expecting a final-page for: ". $cmd );
+			sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+			return true;
+		}
+		return invokeCommand( $postvars['final-page'], $postvars );
+	}
+			
+			
+	if( $cmd=='new-item-bu' ){
+		
+		include_once __DIR__ . "/new-file-bu.php";
+		$db_err = save_bu_item( $postvars );	
+		if($db_err>0){
+			sendHtmlError( "DB error: $db_err" );
+			return true;
+		}
+		
+		if($postvars['final-page'] == '' ){
+			error_log( "Error: Expecting a final-page for: ". $cmd );
+			sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+			return true;
+		}
+		return invokeCommand( $postvars['final-page'], $postvars );
+		
+	}
+			
+	if( $cmd=='edit-item-bu' ){
+		
+		include_once __DIR__ . "/new-file-bu.php";
+		$db_err = save_bu_item( $postvars, $postvars['id'] );	
+		if($db_err>0){
+			sendHtmlError( "DB error: $db_err" );
+			return true;
+		}
+		
+		if($postvars['final-page'] == '' ){
+			error_log( "Error: Expecting a final-page for: ". $cmd );
+			sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+			return true;
+		}
+		return invokeCommand( $postvars['final-page'], $postvars );
+		
+	}
+
+	if( $cmd=='delete-bu' ){
+
+		include_once __DIR__ . "/delete-file-bu.php";
+		$db_err = delete_bu_item( $postvars['id'] );	
+		if($db_err>0){
+			sendHtmlError( "DB error: $db_err" );
+			return true;
+		}
+		
+		if($postvars['final-page'] == '' ){
+			error_log( "Error: Expecting a final-page for: ". $cmd );
+			sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+			return true;
+		}
+		return invokeCommand( $postvars['final-page'], $postvars );
+
+	}
+			
+}
+		
+?>
diff --git a/html/inc/secure.php b/html/inc/secure.php
new file mode 100644
index 0000000..0e1450a
--- /dev/null
+++ b/html/inc/secure.php
@@ -0,0 +1,77 @@
+<?php 
+
+/*if( isset($_POST['action']) ) {
+	error_log("Login action:".$_POST['action'] . "," . $_POST['username'] . "," . $_POST['password'] );
+}*/
+
+session_start();
+
+if( isset($_GET['action']) && $_GET['action']=='logout' ) {
+	unset( $_SESSION['username'] );
+	include_once "index.php";
+	exit;
+}
+
+if( isset($_SESSION['username']) && strlen($_SESSION['username'])>=4 ){
+	//the user is logged in so nothing further to do in this script
+	return;
+}
+
+//is there a login attempt
+if( isset($_POST['username']) && isset($_POST['password']) ){
+	
+	//any request must have the 'action' set to btn_login
+	if( !isset($_POST['action']) || $_POST['action']!='btn_login' ) {
+		error_log("Login Error: Missing or incorrect value for 'action'");
+		exit;
+	}
+	
+	if( !isset($_POST['svr_nonce']) || $_POST['svr_nonce']=='' ) {
+		error_log("Login Error: Missing or blank value for 'svr_nonce'");
+		exit;
+	}	
+
+	//has the correct nonce been returned?
+	if( $_POST['svr_nonce']!=$_SESSION['svr_nonce'] ){
+		$_SESSION['svr_nonce'] = '';	// invalidate nonce
+		error_log("Login Error: Incorrect value for 'svr_nonce'");
+		exit;
+	}	
+
+	include_once "common.php";
+	include_once "login.php";
+	if(authenticate( $_POST['username'], $_POST['password'], $_SESSION['svr_nonce'] ) ){
+		
+		error_log("Login Success: ".$_POST['username']);
+		$_SESSION['username'] = $_POST['username'];
+				
+		//return the backup list page
+		include_once __DIR__ . "/bu_list_content.php";		
+		sendHtmlOk_WithData( [ 'authenticated', getbu_list_content() ] );
+
+	}else{
+		error_log("Login Failure: ".$_POST['username']);
+		// failed authentication
+		// --- Send back the login page ---
+		$_SESSION['svr_nonce'] = generateNonce();
+		sendHtmlOk_WithData( [ 'login', getLogin_content(), $_SESSION['svr_nonce'] ] );
+	}
+	
+	exit;
+}
+
+// no session is set
+// no attempt to login was made
+// likely that the page was just refreshed
+// --- Send back the login page ---
+
+include_once "common.php";
+$_SESSION['svr_nonce'] = generateNonce();
+
+include_once "login.php";
+sendHtmlOk_WithData( [ 'login', getLogin_content(), $_SESSION['svr_nonce'] ] );
+exit;	//end the script to stop further processing by calling files
+
+//$svr_username = $_SESSION['username'];
+
+?>
diff --git a/html/index.php b/html/index.php
new file mode 100644
index 0000000..3232869
--- /dev/null
+++ b/html/index.php
@@ -0,0 +1,42 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Pearcey: Excecutive Backup Panel</title>
+    
+		<link rel="stylesheet" href="css/main.css"/>
+    
+		<script src="./js/common.js"></script>
+		<script src="./js/main.js"></script>
+		
+	</head>
+	<body onload="index_onload()">
+	  
+	<div class="main_page">
+		
+	<div id="shb_Header" class="flex_header"  >
+		<div id="shb_Title" class="shb_Title" ><img src="img/uk-green.png" style="margin-right: 20px; height: 40px;" >Executive Backup Panel <span id="shb_pageName" style="margin-left:30px; font-size: 10px;"></span></div>
+		<a id="anc_login" class="shb_TitleRHS" href="post_handler.php?action=logout" style="margin-right: 10px;"  >Login</a>		
+	</div>
+
+	<div class="page_inner">
+      
+      <div id="page_content"></div>
+
+		<div class="section_header">Panel Help</div>
+
+		<div class="content_section_text">
+
+			<p>For help using this panel, please email john@pearcey.net</p>
+			
+			<p>This backup utility is open source and can be downloaded from <a href="http://pearcey.net:8080/">pearcey.net</a></p>
+			
+			<p>Please report bugs to john@pearcey.net</p>
+		</div>
+
+      </div>
+    </div>
+    
+  </body>
+</html>
diff --git a/html/js/common.js b/html/js/common.js
new file mode 100644
index 0000000..dc25eab
--- /dev/null
+++ b/html/js/common.js
@@ -0,0 +1,101 @@
+
+/**
+ * COPYRIGHT © 2022 JOHN PEARCEY
+ * All rights reserved
+*/
+
+/*
+ * pagename:
+ * 	The name of the server-side script to handle this post.
+ *	fnCallback:
+ * 	The javascript callback function, called after the AJAX call completes.
+ * map_params:
+ * 	A map containing name value pairs required by the script.
+ * 	e.g.
+ * 	map_params = new Map();
+		map_params.set('username', document.getElementById('username').value );
+		map_params.set('pwdhash', document.getElementById('pwd').value );
+ */
+function postData( pagename, fnCallback, map_params ){		
+							
+	//console.log( 'postData called' );
+	
+	let formData = new FormData();		
+	map_params.forEach((value, key, map) => {
+		//if(key=='action') console.log("postData: ", pagename, value );
+		formData.append( key, value );
+	});
+		
+	//https://www.digitalocean.com/community/tutorials/how-to-use-the-javascript-fetch-api-to-get-data
+	//https://jsonplaceholder.typicode.com/users
+	//https://javascript.info/fetch						
+	fetch( pagename, {
+		method: 'POST',
+		body: formData,
+		
+	})
+	/*.then((response) => {
+		return response.json(); */
+
+	.then(response => response.text()) 
+    .then((dataStr) => {
+		
+		//console.log(dataStr);
+		
+		//there's a whole bunch of warnings and errors that might be sent by PHP. It is not possible
+		//to catch them in the serverside PHP code. So here we search for our json encoded return string which
+		//must be either Error or OK.
+		let pos = dataStr.indexOf("[\"OK\"");
+		let additional=null;
+		if(pos==-1){
+			pos = dataStr.indexOf("[\"Error\"");
+			if(pos==-1){
+				//the PHP is pretty fucked up if you get here!
+				console.warn( dataStr );
+				let encodedStr = JSON.stringify( ["Illegal return from server. Did not find Error or OK.", dataStr ] );
+				return JSON.parse( encodedStr );
+			}
+		}			
+		//console.log(dataStr);
+      return JSON.parse(dataStr);
+        		
+	}).then((data) => {
+		fnCallback( true, data, map_params );
+	
+	}).catch(function(error) {
+		console.log( error );
+		fnCallback( false, error, map_params );
+	});
+	
+}
+
+function checkReturn( success, data, bGetErr=false ){
+
+	if(!success){
+		console.warn( "callback error: " + data );
+		return false;
+	}
+
+	if(data[0]=="Error"){
+		if(bGetErr) return data;
+		alert( "Error: " + data[1] );
+		for( i=2; i<data.length; i++){
+			console.warn( data[i] );
+		}		
+		return false;
+	}
+
+	if(data[0]!="OK"){
+		console.warn( "Unkown server return string ", data );
+		if(bGetErr) return data;
+		return false;
+	}
+	if(bGetErr) return null;
+	return true;
+}
+
+function stripHtml( txt ){	
+	txt =	txt.replace(/<div>/gi, '\n');	
+	txt =	txt.replace(/<\/?[^>]+>/gi, ''); //strip HTML	
+	return txt;
+}
diff --git a/html/js/main.js b/html/js/main.js
new file mode 100644
index 0000000..78fb441
--- /dev/null
+++ b/html/js/main.js
@@ -0,0 +1,454 @@
+
+/**
+ * COPYRIGHT © 2022 JOHN PEARCEY
+ * All rights reserved
+*/
+
+/**
+ * The last nonce we just got from the server
+ */
+var g_svr_nonce = [];
+
+/**
+ * Every 10secs, we get the state of the backup list from the server. This is so that we can keep the front end up-to-date
+ * for the user. I'm sure that there are better ways with modern HTML5 but polling is easy and the application is not
+ * time critical. This is sufficient.
+ */
+var g_bu_list_state = [];
+
+/**
+ * This var g_stack_pages contains the app navigation pages in order. The top most page being the
+ * current page. So, to go back, you would remove the top most page and navigate to the new top page.
+*/
+var g_stack_pages = [];
+	
+function clear_page_state(){
+	g_bu_list_state = [];
+	g_stack_pages = [];
+}
+			
+function push_page( pagename ){
+	g_stack_pages.push( pagename );
+}
+
+/**
+ * Pops the current page off the stack and returns the penultimate one.
+ */
+function back_page( ){
+	g_stack_pages.pop( );
+	return g_stack_pages[g_stack_pages.length - 1];
+}
+
+function index_onload(){	
+	update_bu_state( false );
+	set_main_content( true );	
+}
+
+function update_bu_state( bCheckPage = true ){
+	
+	setTimeout( () => { 
+		update_bu_state();
+	}, 10000);
+
+	if(bCheckPage){
+		//console.log( "update_bu_state pages:", g_stack_pages );
+		if( g_stack_pages[g_stack_pages.length - 1] != 'backup-list' ) return;
+	}
+
+	//console.log( "update_bu_state polling" );
+
+	map_params = new Map();
+	map_params.set('action', 'backup-list-state' );
+	
+	postData( "post_handler.php", function( success, data, m ){
+			
+		if(data[1][0]=='login'){
+			console.log( 'Cannot get backup-list-state: Not logged in' );
+			return;
+		}		
+
+		let new_state = data[1][1];
+		
+		//console.log( 'backup-list-state' + new_state );
+		
+		if( g_bu_list_state.length == 0 ){
+			//just store the new state
+			g_bu_list_state = new_state;
+			return;
+		}
+		
+		//compare the two arrays, if different length, then refresh the UI.
+		if( g_bu_list_state.length != new_state.length ){
+			g_bu_list_state = new_state;
+			set_main_content( false );
+			return;
+		}
+			
+		//check each row in turn
+		let bFound = false;
+		for( j=0; j<g_bu_list_state.length; j++){
+			curr_state_j = g_bu_list_state[j];
+			bFound = false;
+			
+			//console.log("looking for ID = " + curr_state_j[0] );				
+			
+			for( i=0; i<new_state.length; i++){
+				
+				new_state_i = new_state[i];						
+				//console.log("compare with", new_state_i );
+				
+				
+				if( new_state_i[0] == curr_state_j[0]){							
+					bFound = (new_state_i[1] == curr_state_j[1]);
+					break;
+				}
+			}
+			if(!bFound){
+				//at least one ID or value does not match
+				g_bu_list_state = new_state;
+				set_main_content( false );
+				break;
+			}
+		}
+		
+	}, map_params );
+
+}
+
+
+
+
+function set_main_content( bPushPage ){
+
+	//console.log("3");
+
+	map_params = new Map();
+	map_params.set('action', 'backup-list' );
+	
+	postData( "post_handler.php", function( success, data, m ){
+			
+		if(data[0]!="OK"){
+			console.log( data );
+			return;
+		}
+
+		let elem = document.getElementById('page_content');
+		
+		//display page from server
+		[ page_type, elem.innerHTML ] = data[1];
+		console.log('set_main_content page_type: ', page_type );
+		
+		if(page_type=='login'){
+			//clear client side data
+			clear_page_state();
+			g_svr_nonce = data[1][2];
+			return;
+		}
+		
+		if(bPushPage) push_page('backup-list');
+		
+	}, map_params );
+			
+}
+
+/**
+ * Button clicked and we require a new page from the server to be placed in the page_content area.
+ * instr(uction) is usually the page name except where a 'back' navigation is required.
+ */
+async function btn_clk_nav( elem, instr, id=0 ){
+
+	// instr can now also be a space separated list of instructions
+	//we split them out
+	instrArr = instr.split(" ");
+	instr = instrArr[0];
+	
+	//noTest
+
+	map_params = new Map();
+	map_params.set('id', id );	
+	
+	let b_is_db_type = false;
+	
+	switch(instr){
+		
+	case 'btn_login':
+		if( !valid_btn_login() )		return;
+		await set_btn_login( map_params );		
+		map_params.set('svr_nonce', g_svr_nonce );		
+		map_params.set('final-page', 'backup-list' );
+		navigate_page( instr, map_params, false );			
+		return;
+		
+	case 'btn_run_bu':
+		if(instrArr.length==2 && instrArr[1]=='noTest'){
+			map_params.set('isTest', 'false' );
+		}else{
+			map_params.set('isTest', 'true' );
+			console.log("running a test backup");
+		}
+		exec_instruction( instr, map_params );
+		return;
+	
+	case 'delete-bu':
+		if( !confirm('Are you sure you wish to delete backup item ' + id ) ) return;
+		map_params.set('final-page', 'backup-list' );
+		navigate_page( instr, map_params, false );
+		return;
+		
+	case 'btn_edit_shed_bu':
+		if( !valid_btn_edit_shed_bu() )		return;		
+		set_btn_edit_shed_bu( map_params );		
+		map_params.set('final-page', back_page() );	//server to return previous page (usually backup-list) after processing
+		navigate_page( 'edit_shed_bu', map_params, false );
+		return;
+	
+	case 'btn_new_bu_db':
+		b_is_db_type=true;
+		//drop through
+	case 'btn_new_bu_file':		
+		if( !valid_btn_new_bu( b_is_db_type ) )	return;
+		set_btn_new_bu( b_is_db_type, map_params );
+		map_params.set('final-page', back_page() );	//server to return previous page (usually backup-list) after processing
+		navigate_page( 'new-item-bu', map_params, false );
+		return;
+		
+	case 'btn_edit_bu_db':
+		b_is_db_type=true;
+		//drop through
+	case 'btn_edit_bu_file':		
+		if( !valid_btn_new_bu( b_is_db_type ) )	return;
+		set_btn_new_bu( b_is_db_type, map_params );
+		map_params.set('final-page', back_page() );	//server to return previous page (usually backup-list) after processing
+		navigate_page( 'edit-item-bu', map_params, false );
+		return;
+
+	case 'btn_cancel_new_bu':
+	case 'btn_cancel':
+		navigate_page( back_page(), map_params, false );
+		return;		
+	}
+	
+	navigate_page( instr, map_params, true );
+}
+
+
+function navigate_page( goto_page, map_params, bPushPage ){
+
+	//console.log( 'navigate_page: ', goto_page );
+	
+	map_params.set('action', goto_page );
+	//map_params.set('updateList', JSON.stringify( usrUpdates )  );
+	
+	postData( "post_handler.php", function( success, data, m ){
+		
+		if(data[0]!="OK"){
+			console.log( data[0], data[1] );
+			if(!bPushPage){
+				//we need to put back the page that we dropped off or maybe an error page
+				//but use goto_page for now
+				push_page( goto_page ); 
+			}
+			return;
+		}
+		
+		
+		let elem = document.getElementById('page_content');
+		[ page_type, elem.innerHTML ] = data[1];		
+		console.log('navigate_page page_type: ', page_type);
+		
+		if(page_type=='login'){
+			//clear client side data
+			clear_page_state();
+			g_svr_nonce = data[1][2];
+			return;
+		}
+
+		if(page_type=='authenticated'){
+			elem = document.getElementById('anc_login');
+			elem.innerHTML="Logout";			
+			push_page( map_params.get('final-page') );
+			return;
+		}
+
+		if(bPushPage)	push_page( goto_page );		
+		
+	}, map_params );
+			
+}
+
+function setTextAreaFromArray( elem, arr ){	
+	elem.value = arr.toString().replaceAll(',', "\r\n" );	
+}
+
+function exec_instruction( instr, map_params ){
+
+	console.log( 'exec_instruction: ', instr );
+	let id = map_params.get('id');
+	
+	map_params.set('action', instr );
+	//map_params.set('updateList', JSON.stringify( usrUpdates )  );
+	
+	postData( "post_handler.php", function( success, data, m ){
+		
+		if(data[0]!="OK"){
+			console.log( data );
+			return;
+		}
+		
+		switch(instr){
+		case 'btn_run_bu':
+		
+			if(data[1][0]=='done'){
+				// this comes from the test run, although I should really explicitly send back the 'test' flag				
+				let elem = document.getElementById( "ta_testoutput" );
+				if(elem==null){
+					console.log( data );
+					return;
+				}
+				setTextAreaFromArray( elem, data[1] );
+				return;
+			}
+			
+			if(data[1][0]!='running'){
+				console.log("odd message: " + data[1][0] );
+				console.log( data );
+				return;
+			}
+			
+			//set the tick	
+			let elem = document.getElementById( "td_run_" + id );
+			elem.innerHTML = "<img id=\"td_run_\"" + id + " src=\"img/green-tick.png\">";
+			
+			//ensure the state map is also adjusted
+			for( j=0; j<g_bu_list_state.length; j++){				
+				if(g_bu_list_state[j][0]==id){
+					g_bu_list_state[j][1] = 1;
+				}
+			}
+		}
+		
+	}, map_params );
+			
+}
+
+function valid_btn_login(){
+	
+	let login_name = document.getElementById('login_name').value;
+	if(login_name=='') {
+		alert("Please enter your login name");
+		return false;
+	}
+	
+	let login_pwd = document.getElementById('login_pwd').value;
+	if(login_pwd=='') {
+		alert("Please enter your password");
+		return false;
+	}
+	return true;
+}
+
+/**
+ * Collect data from login form
+ */
+async function set_btn_login( map_params ){
+	
+	let raw_pwd = document.getElementById('login_pwd').value;
+	const pwd_hash = await getHash256(raw_pwd+g_svr_nonce);	
+	map_params.set( 'username', document.getElementById('login_name').value );	
+	map_params.set( 'password', pwd_hash );
+}
+
+async function getHash256(message) {
+	//https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+	const encoder = new TextEncoder();
+	const data = encoder.encode(message);
+	const hashBuffer = await window.crypto.subtle.digest("SHA-256", data);  
+	const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
+	const hashHex = hashArray
+		.map((b) => b.toString(16).padStart(2, "0"))
+		.join(""); // convert bytes to hex string
+	return hashHex;
+}
+
+/**
+ * Validate the form for a new backup entry
+ * b_is_db_type is true for DB entry and false for file entry.
+ */
+function valid_btn_new_bu( b_is_db_type ){
+	return true;
+}
+
+/**
+ * Collect the form data together.
+ * b_is_db_type is true for DB entry and false for file entry.
+ */
+function set_btn_new_bu( b_is_db_type, map_params ){
+	
+	map_params.set( 'new_bu_is_dbType', b_is_db_type?1:0 );
+	map_params.set( 'new_bu_name', document.getElementById('new_bu_name').value );
+	map_params.set( 'new_bu_source', document.getElementById('new_bu_source').value );	
+	map_params.set( 'new_bu_dest', document.getElementById('new_bu_dest').value );
+	if(!b_is_db_type){
+		map_params.set( 'new_bu_exf', document.getElementById('new_bu_exf').value );
+	}
+}
+
+/**
+ * Validate the form for the backup scheduling page
+ */
+function valid_btn_edit_shed_bu(){
+	
+	sched_date = document.getElementById('sched_date').value;
+	if(sched_date=='') {
+		alert("Please choose a date");
+		return false;
+	}
+	
+	sched_time = document.getElementById('sched_time').value;
+	if(sched_time=='') {
+		alert("Please choose a time");
+		return false;
+	}
+	
+	let dt_chosen = new Date( sched_date + 'T' + sched_time + ':00Z');
+	let now = new Date();
+	
+	if(dt_chosen<=now){
+		alert("Please choose a time in the future, not the past: ", dt_chosen, now );
+		return false;
+	}
+	
+	console.log("dates: ", dt_chosen, now );
+	
+	return true;
+}
+	
+/**
+ * Collect the form data together for the backup scheduling page
+ */
+function set_btn_edit_shed_bu( map_params ){
+		
+	map_params.set( 'sched_date', document.getElementById('sched_date').value );	
+	map_params.set( 'sched_time',  document.getElementById('sched_time').value );	
+	
+	switch(document.querySelector('input[name="sched_period"]:checked').id){
+	case 'sched_ip_daily':
+		map_params.set( 'sched_period', 'D' );
+		break;
+
+	case 'sched_ip_weekly':
+		map_params.set( 'sched_period', 'W' );
+		break;
+
+	case 'sched_ip_monthly':
+		map_params.set( 'sched_period', 'M' );
+		break;
+		
+	case 'sched_ip_none':
+	default:
+		map_params.set( 'sched_period', 'N' );
+	}
+	
+	
+}
+
diff --git a/html/phpinfo.php b/html/phpinfo.php
new file mode 100644
index 0000000..cf60860
--- /dev/null
+++ b/html/phpinfo.php
@@ -0,0 +1,3 @@
+<?php
+phpinfo();
+?>
diff --git a/html/post_handler.php b/html/post_handler.php
new file mode 100644
index 0000000..bce003d
--- /dev/null
+++ b/html/post_handler.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * COPYRIGHT © 2024 JOHN PEARCEY
+ * All rights reserved
+*/
+
+//error_log("POST: " . print_r($_POST, true) );
+
+include_once __DIR__ . "/inc/secure.php";
+include_once __DIR__ . "/inc/common.php";
+
+if( !isset($_POST['action']) ){
+	error_log("Post Error: Missing value for 'action'");
+	exit;
+}
+
+//error_log("post_handler.php: " . $_POST['action'] );
+
+include_once __DIR__ . "/inc/pagemap.php";
+if( invokeCommand( $_POST['action'], $_POST ) ) exit;
+		
+error_log( "Error: Illegal value for action: ". $_POST['action'] );
+sendHtmlError( "Illegal call to " . __FILE__ );
+		
+?>