diff --git a/exec/lbshell.js b/exec/lbshell.js
index ac6553264e0b173b30f65e289f88fd9e8b565980..9415fad1438aaf4e1f61b6c80636c43d37b5308d 100644
--- a/exec/lbshell.js
+++ b/exec/lbshell.js
@@ -707,18 +707,8 @@ while(bbs.online) {
 					}
 					stop_mouse();
 					clear_screen();
-					if(file_exists("../xtrn/doorscan/doorscan.js")) {
-						try {
-    						load("../xtrn/doorscan/doorscan.js","run",xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].code);
-						}
-						catch(e) {
-    						console.writeln("DOORSCAN ERROR: "+e);
-    						log("Error running "+xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].code+" "+e);
-						}
-					}
-					else {
-						bbs.exec_xtrn(xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].number);
-					}
+
+					bbs.exec_xtrn(xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].number);
 					start_mouse();
 					draw_main(true);
 					xtrnsec.draw();
diff --git a/webv4/pages/More/003-doorscan.xjs b/webv4/pages/More/003-doorscan.xjs
new file mode 100644
index 0000000000000000000000000000000000000000..ee5571029b56498bb33db208b25551f691e96f19
--- /dev/null
+++ b/webv4/pages/More/003-doorscan.xjs
@@ -0,0 +1,70 @@
+<!-- HIDDEN:Door Statistics -->
+<html>
+<head>
+  <title>Monthly Door Log for <?xjs write(system.name) ?></title>
+</head>
+<body>
+<?xjs 
+   load("/sbbs/xtrn/doorscan/doorscan.js");
+   var lp=new LogParser(); 
+   
+   if (typeof settings.xtrn_blacklist === 'string') {
+       settings.xtrn_blacklist = settings.xtrn_blacklist.toLowerCase().split(',');
+   } else {
+       settings.xtrn_blacklist = [];
+   }
+   settings.xtrn_blacklist.push('doorscfg');
+   settings.xtrn_blacklist.push('doorstat');
+   settings.xtrn_blacklist.push('doorsta');
+
+   var doors = [];
+   xtrn_area.sec_list.forEach(function (sec) {
+       if (!sec.can_access || sec.prog_list.length < 1) return;
+       if (settings.xtrn_blacklist.indexOf(sec.code.toLowerCase()) > -1) return;
+       sec.prog_list.forEach(function (prog) {
+           if (!prog.can_access || !prog.can_run) return;
+           if (settings.xtrn_blacklist.indexOf(prog.code.toLowerCase()) > -1) return;
+           doors.push(prog.code);
+       });
+   });
+
+?>
+
+  <div class="well well-sm"><h3>Door Statistics</h3></div>
+
+  <div class="list-group" style="margin-top:1em;">
+
+  <table class="table table-condensed table-responsive">
+    <tr>
+      <th>Door Name</th>
+      <th>Users</th>
+      <th>Times Played</th>
+      <th>Time Spent Playing</th>
+    <tr>
+<?xjs
+	var alldoors=new Array();
+	var since=new Date();
+	since.setMonth(since.getMonth()-1);
+	doors.forEach(function (door) {
+		var tr=lp.usersOfSince(door, since);
+		if(tr.total > 0)
+			alldoors.push(tr);	
+	});
+	alldoors=alldoors.sort(function (a,b) {
+			return(b.total-a.total);
+		});
+	for(door in alldoors) {
+?>
+    <tr>
+      <td><?xjs write(xtrn_area.prog[alldoors[door].prog].name) ?></td>
+      <td align="center"><?xjs write(alldoors[door].users) ?></td>
+      <td align="center"><?xjs write(alldoors[door].total) ?></td>
+      <td align="center"><?xjs write(system.secondstr(alldoors[door].total_duration)) ?></td>
+    </tr>
+<?xjs
+	}
+?>
+  </table>
+  </div>
+</body>
+</html>
diff --git a/xtrn/doorscan/doorscan.js b/xtrn/doorscan/doorscan.js
index 1dcab8371c0cf3e542a98cd3872e3921c645a049..33916c6f983c9570c8c48e60d17cc4e05cf965c5 100644
--- a/xtrn/doorscan/doorscan.js
+++ b/xtrn/doorscan/doorscan.js
@@ -2,10 +2,7 @@ load("sbbsdefs.js");
 load("text.js");
 load("lockfile.js");
 
-var doorscan_dir='.';
-try { throw barfitty.barf(barf) } catch(e) { doorscan_dir=e.fileName }
-doorscan_dir=doorscan_dir.replace(/[\/\\][^\/\\]*$/,'');
-doorscan_dir=backslash(doorscan_dir);
+var doorscan_dir = js.exec_dir;
 
 function LockedOpen(filename, fmode)
 {
@@ -156,30 +153,30 @@ function Display_LORD(filename)
 					case 'r':
 						ch=txt.charAt(++i);
 						switch(ch) {
-						case '0':
-							console.attributes=BG_BLACK;
-							break;
-						case '1':
-							console.attributes=BG_BLUE;
-							break;
-						case '2':
-							console.attributes=BG_GREEN;
-							break;
-						case '3':
-							console.attributes=BG_CYAN;
-							break;
-						case '4':
-							console.attributes=BG_RED;
-							break;
-						case '5':
-							console.attributes=BG_MAGENTA;
-							break;
-						case '6':
-							console.attributes=BG_BROWN;
-							break;
-						case '7':
-							console.attributes=BG_LIGHTGRAY;
-							break;
+							case '0':
+								console.attributes=BG_BLACK;
+								break;
+							case '1':
+								console.attributes=BG_BLUE;
+								break;
+							case '2':
+								console.attributes=BG_GREEN;
+								break;
+							case '3':
+								console.attributes=BG_CYAN;
+								break;
+							case '4':
+								console.attributes=BG_RED;
+								break;
+							case '5':
+								console.attributes=BG_MAGENTA;
+								break;
+							case '6':
+								console.attributes=BG_BROWN;
+								break;
+							case '7':
+								console.attributes=BG_LIGHTGRAY;
+								break;
 						}
 						break;
 					case 'c':
@@ -557,7 +554,7 @@ function UserConfig_configure_sec(dcfg, sec)
 					r=ascii('N');
 			}
 			console.uselect(index.length,"External                                News  Scores  Run Count"
-					,format("%-40s   %c      %c         %c",xtrn_area.prog[door].name,n,s,r));
+				,format("%-40s   %c      %c         %c",xtrn_area.prog[door].name,n,s,r));
 		}
 		if(!index.length)
 			return;
@@ -585,8 +582,8 @@ function UserConfig_configure_sec(dcfg, sec)
 		else
 			this.door[index[xprog-1]].skipRunCount=!console.yesno("Show run counts");
 		if(this.door[index[xprog-1]].skipRunCount
-				&& this.door[index[xprog-1]].skipScore
-				&& this.door[index[xprog-1]].skipNews)
+			&& this.door[index[xprog-1]].skipScore
+			&& this.door[index[xprog-1]].skipNews)
 			delete this.door[index[xprog-1]];
 	}
 }
@@ -797,7 +794,7 @@ function sysop_get_newstype(dflt)
 	if(i < 1 || i > index.length)
 		return(dflt);
 	return(index[i-1]);
-	
+
 }
 
 function sysop_get_scorestype(dflt)
@@ -907,8 +904,8 @@ function sysop_config_skip(dcfg)
 	while(1) {
 		for(sec in xtrn_area.sec) {
 			index.push(sec);
-			console.uselect(index.length, "External Program Section", 
-					format("%-40s %s",xtrn_area.sec[sec].name
+			console.uselect(index.length, "External Program Section",
+				format("%-40s %s",xtrn_area.sec[sec].name
 					,(dcfg.skipSection[sec] != undefined && dcfg.skipSection[sec])?"Skip":"Include"));
 		}
 		if(index.length==0)
@@ -945,6 +942,7 @@ function sysop_config()
 	}
 }
 
+// deprecated, use pre/post instead 
 function runXtrn(xtrn)
 {
 	if(xtrn_area.prog[xtrn]==undefined)
@@ -972,7 +970,7 @@ function runXtrn(xtrn)
 	ucfg.save();
 
 	if(!(dcfg.door[xtrn]!=undefined && dcfg.door[xtrn].skip != undefined && dcfg.door[xtrn].skip)
-			|| !(dcfg.skipSection[xtrn_area.prog[xtrn].sec_code]!=undefined && dcfg.skipSection[xtrn_area.prog[xtrn].sec_code])) {
+		|| !(dcfg.skipSection[xtrn_area.prog[xtrn].sec_code]!=undefined && dcfg.skipSection[xtrn_area.prog[xtrn].sec_code])) {
 		bbs.log_str("DOORSCAN - "+xtrn+" starting @ "+now.toString()+"\r\n");
 	}
 
@@ -982,7 +980,7 @@ function runXtrn(xtrn)
 	dcfg=new DoorConfig(true);
 
 	if(!(dcfg.door[xtrn]!=undefined && dcfg.door[xtrn].skip != undefined && dcfg.door[xtrn].skip)
-			&& !(dcfg.skipSection[xtrn_area.prog[xtrn].sec_code]!=undefined && dcfg.skipSection[xtrn_area.prog[xtrn].sec_code])) {
+		&& !(dcfg.skipSection[xtrn_area.prog[xtrn].sec_code]!=undefined && dcfg.skipSection[xtrn_area.prog[xtrn].sec_code])) {
 		bbs.log_str("DOORSCAN - "+xtrn+" ending @ "+now.toString()+"\r\n");
 	}
 
@@ -998,6 +996,60 @@ function runXtrn(xtrn)
 	ucfg.save();
 }
 
+function runPre(xtrn)
+{
+	if(xtrn_area.prog[xtrn]===undefined)
+		throw("Unknown external: "+xtrn);
+	if(!xtrn_area.prog[xtrn].can_run)
+		throw("User "+user.name+" is not allowed to run "+xtrn);
+
+	var now=new Date();
+	var dcfg=new DoorConfig(true);
+
+	dcfg.door[xtrn].lastRan=now;
+	dcfg.door[xtrn].runCount++;
+	dcfg.save();
+
+	var ucfg=new UserConfig(user.number, true);
+	if(ucfg.door[xtrn] === undefined) {
+		if(ucfg.global === undefined || (!ucfg.global.noAutoScan)) {
+			ucfg.addxtrn(xtrn);
+		}
+	}
+
+	if(ucfg.door[xtrn] !== undefined)
+		ucfg.door[xtrn].lastRan=now;
+
+	ucfg.save();
+
+	if(!(dcfg.door[xtrn]!==undefined && dcfg.door[xtrn].skip !== undefined && dcfg.door[xtrn].skip)
+		|| !(dcfg.skipSection[xtrn_area.prog[xtrn].sec_code]!==undefined && dcfg.skipSection[xtrn_area.prog[xtrn].sec_code])) {
+		bbs.log_str("DOORSCAN - "+xtrn+" starting @ "+now.toString()+"\r\n");
+	}
+}
+
+function runPost(xtrn)
+{
+	now=new Date();
+	dcfg=new DoorConfig(true);
+
+	if(!(dcfg.door[xtrn]!==undefined && dcfg.door[xtrn].skip !== undefined && dcfg.door[xtrn].skip)
+		&& !(dcfg.skipSection[xtrn_area.prog[xtrn].sec_code]!==undefined && dcfg.skipSection[xtrn_area.prog[xtrn].sec_code])) {
+		bbs.log_str("DOORSCAN - "+xtrn+" ending @ "+now.toString()+"\r\n");
+	}
+
+	dcfg.door[xtrn].lastExit=now;
+	dcfg.save();
+
+	ucfg=new UserConfig(user.number, true);
+	if(ucfg.door[xtrn] !== undefined) {
+		ucfg.door[xtrn].lastExit=now;
+		ucfg.door[xtrn].lastRunCount=dcfg.door[xtrn].runCount;
+	}
+
+	ucfg.save();
+}
+
 function doScan()
 {
 	var dcfg=new DoorConfig();
@@ -1020,7 +1072,7 @@ function doScan()
 			continue;
 		if(dcfg.door[door].installed > ucfg.global.lastScan) {
 			/* This door is NEW! */
-			
+
 			/* If the user can't run it, don't display it. */
 			if(!xtrn_area.prog[door].can_run)
 				continue;
@@ -1079,7 +1131,7 @@ function doScan()
 					 * If the news file has not been updated, don't bother
 					 * Some doors only update the news during maintenance
 					 */
-					
+
 					if(new Date(file_date(dcfg.door[door].news)*1000) >= scantime) {
 						/* Assume ANSI */
 						if(dcfg.door[door].newsType==undefined)
@@ -1100,7 +1152,7 @@ function doScan()
 					 * If the Scores file has not been updated, don't bother
 					 * Some doors only update the Scores during maintenance
 					 */
-					
+
 					if(new Date(file_date(dcfg.door[door].score)*1000) >= scantime) {
 						/* Assume ANSI */
 						if(dcfg.door[door].scoreType==undefined)
@@ -1156,11 +1208,15 @@ for(i in argv) {
 		case 'scan':
 			doScan();
 			break;
+		case 'pre':
+			runPre(argv[++i].toLowerCase());
+			break;
+		case 'post':
+			runPost(argv[++i].toLowerCase());
+			break;
 		case 'run':
-			if(i+1<argc)
-				runXtrn(argv[++i].toLowerCase());
-			else
-				throw("XTRN code not included on command-line!");
+			// deprecated
+			runXtrn(argv[++i].toLowerCase());
 			break;
 		case 'config':
 			new UserConfig(user.number).configure();
@@ -1171,5 +1227,7 @@ for(i in argv) {
 		case 'rank':
 			// TODO: Door popularity rankings
 			break;
+		default:
+			break;
 	}
 }
diff --git a/xtrn/doorscan/doorscan.txt b/xtrn/doorscan/doorscan.txt
index fe28138102d4da2f082d61f9d5043befc96ab9d7..0dd122212dac3cd5f4a33a327c6137d5ce410951 100644
--- a/xtrn/doorscan/doorscan.txt
+++ b/xtrn/doorscan/doorscan.txt
@@ -1,18 +1,45 @@
+This version of doorscan has been updated to work with the new pre/post 
+xtrn modules, and a file locking issue that would crash webv4 has also been
+fixed. A new webv4 module has been added. No other functionality has been added.
+
+Current unfinished status of doorscan:
+1. It will scan for door usage and display it on the web page only
+2. There is no logon/terminal module to display door usage
+3. The bulletin functionality was never implemented
+4. The user preferences may not be fully implemented
+5. It really ought to be converted to use JSONDB
+
+-----
+
 DoorScan is a utility to notify users of which externals have been ran, how
 often, and by whom.
 
 To function properly, doorscan must be used to launch all the monitored doors
 on your BBS.  Integrating doorscan into your BBS is done as follows:
 
-Setting up the login event:
-===========================
-The actual scan should be ran during the users login.  There are a few
-different ways of accomplishing this:
-1) As a logon fixed event:
+1. Add to modopts.ini under [xtrn_sec]
+
+eval_before_exec=js.exec("/sbbs/xtrn/doorscan/doorscan.js",{},"pre",prog.code);
+eval_after_exec=js.exec("/sbbs/xtrn/doorscan/doorscan.js",{},"post",prog.code);
+
+(If you are upgrading from a prior version of doorscan, it should still work but the
+preferred method would be to remove the doorscan.js run call from your shell and use
+the above hooks)
+
+2. Scan for new doors
+
+The command to scan for new doors is:
+/sbbs/exec/jsexec.js /sbbs/xtrn/doorscan/doorscan.js scan
+
+You have many options:
+-Run it manually when you add a door
+-Add it to crontab (for Linux)
+-Run it as a fixed login or daily event. Example:
+
 	SCFG -> External Programs -> Fixed Events -> Logon Event
 	Set to "*../xtrn/doorscan/doorscan.js scan"
 
-2) As an External
+-Add it as an external to be run manually:
 	SCFG -> External Programs -> Online Programs (Doors) -> Main 
 	-> Available Online Programs -> Add
 
@@ -34,26 +61,18 @@ different ways of accomplishing this:
 	BBS Drop File Type         None                         
 	Place Drop File In         Node Directory               
 
-3) In your logon script.
-	For logon.js, add the following line where appropriate:
-	load("../xtrn/doorscan/doorscan.js","scan");
-
-Integrating Doorscan Into your Shell
-====================================
-If your shell uses xtrn_sec.js (as most do) edit xtrn_sec.js and look for the
-line "bbs.exec_xtrn(xtrn_area.sec_list[xsec].prog_list[i].code);"
-Comment out that line and replace it with:
-//bbs.exec_xtrn(xtrn_area.sec_list[xsec].prog_list[i].code);
-try {
-    load("../xtrn/doorscan/doorscan.js","run",xtrn_area.sec_list[xsec].prog_list[i].code);
-}
-catch(e) {
-    console.writeln("DOORSCAN ERROR: "+e);
-    log("Error running "+xtrn_area.sec_list[xsec].prog_list[i].code+" "+e);
-}
-
-If your shell does not use xtrn_sec.js, you should be able to figure out how to
-modify it to accomlish the same end.
+3. Enable the web interface
+
+For original web (ie runemaster), copy doorscan.xjs to your web root
+
+For webv4, edit webv4/pages/More/003-doorscan.xjs and remove the HIDDEN from
+the comment:
+
+<!-- HIDDEN:Door Statistics -->
+
+so it becomes
+
+<!-- Door Statistics -->
 
 Adding a Door Scan Configuration Item
 =====================================