diff --git a/.gitignore b/.gitignore
index 3c3629e647f5ddf82548912e337bea9826b434af..9108365fc5839a9d46bf30d8dba75b606ba8d1a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
 node_modules
+.DS_Store
+*.swp
+*.swo
+*.bak
+*~
diff --git a/fabmo/Makefile b/fabmo/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..db6b4f66c71519b47cff3959c16c54fd301f0784
--- /dev/null
+++ b/fabmo/Makefile
@@ -0,0 +1,10 @@
+build/fabmo-mods-app.fma : clean
+	mkdir -p build
+	zip -r build/fabmo-mods-app.fma index.html package.json icon.png css js
+	cd ..; \
+	zip -r fabmo/build/fabmo-mods-app.fma package.json files.html js modules programs 
+
+clean:
+	rm -f build/fabmo-mods-app.fma
+
+.PHONY: clean
diff --git a/fabmo/build/icon.png b/fabmo/build/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..42d5057b48fa09f8991abd0d886c9af9d7fb7c83
Binary files /dev/null and b/fabmo/build/icon.png differ
diff --git a/fabmo/css/style.css b/fabmo/css/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..be5688c6ee10cde5f64122fd60462511d115e972
--- /dev/null
+++ b/fabmo/css/style.css
@@ -0,0 +1,45 @@
+body {
+	background: #fff;
+	font-family: monospace;
+	font-size: 11px;
+}
+.modal {
+    display: none; /* Hidden by default */
+    position: fixed; /* Stay in place */
+    z-index: 1; /* Sit on top */
+    left: 0;
+    top: 0;
+    width: 100%; /* Full width */
+    height: 100%; /* Full height */
+    overflow: auto; /* Enable scroll if needed */
+    background-color: rgb(0,0,0); /* Fallback color */
+    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
+}
+
+/* Modal Content/Box */
+.modal-content {
+    background-color: #fefefe;
+    margin: 5% auto; /* 15% from the top and centered */
+    padding: 20px;
+    border: 1px solid #888;
+    width: 80%; /* Could be more or less, depending on screen size */
+    min-height: 200px;
+    max-height: 70%;
+    border-radius: 10px;
+    overflow: scroll;
+}
+
+/* The Close Button */
+.close {
+    color: #aaa;
+    float: right;
+    font-size: 28px;
+    font-weight: bold;
+}
+
+.close:hover,
+.close:focus {
+    color: black;
+    text-decoration: none;
+    cursor: pointer;
+}
diff --git a/fabmo/icon.png b/fabmo/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..42d5057b48fa09f8991abd0d886c9af9d7fb7c83
Binary files /dev/null and b/fabmo/icon.png differ
diff --git a/fabmo/index.html b/fabmo/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..e4ec3a15714c79d1f745a6573f9a83487846a3bb
--- /dev/null
+++ b/fabmo/index.html
@@ -0,0 +1,31 @@
+<html>
+<head><meta charset="utf-8">
+<title>mods</title>
+<link rel="stylesheet" href="css/style.css">
+</head>
+<body link="black" alink="black" vlink="black">
+<script src="js/fabmo.js"></script>
+<script type="text/javascript">
+	var fabmo = new FabMoDashboard();
+</script>
+<script src="js/fabmo-mods.js"></script>
+<script src="js/mods.js"></script>
+
+<!-- The Modal -->
+<div id="myModal" class="modal">
+  <!-- Modal content -->
+  <div class="modal-content">
+    <div id="modal-text">
+
+    </div>
+  </div>
+</div>
+
+</body>
+<script type="text/javascript">
+/*
+	showModal({ 
+		html : "X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>X<br/>"
+	});*/
+</script>
+</html>
\ No newline at end of file
diff --git a/fabmo/js/fabmo-mods.js b/fabmo/js/fabmo-mods.js
new file mode 100644
index 0000000000000000000000000000000000000000..f4a8f1b28991892b87a1d70f3276145245806154
--- /dev/null
+++ b/fabmo/js/fabmo-mods.js
@@ -0,0 +1,150 @@
+window._open = window.open 
+window._close = window.close
+window._alert = window.alert
+
+var fakeWindow = {
+	document  : {
+		createTextNode : function(args) {return window.document.createTextNode(...arguments);},
+		createElement : function(args) {return window.document.createElement(...arguments);},
+		getElementById : function(args) {return window.document.createElement(...arguments);}
+	},
+	close : function() {
+		hideModal();
+	}
+};
+
+window.alert = function(msg) {
+	showModal({html : '<h2>Alert!</h2><p>' + msg + '</p>'})
+}
+
+function showModal(options) {
+	var modalContent = document.getElementById('modal-text');
+	if('html' in options) {
+		modalContent.innerHTML = options['html']
+	}
+	fakeWindow.document.body = modalContent;
+	var modal = document.getElementById('myModal');
+
+	 modal.style.display = "block";
+}
+
+function hideModal() {
+	var modal = document.getElementById('myModal')
+	modal.style.display = "none"
+}
+
+function httpGet(theUrl, callback)
+{
+    var xmlHttp = new XMLHttpRequest();
+    xmlHttp.onreadystatechange = function() { 
+        if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
+            callback(xmlHttp.responseText);
+    }
+    xmlHttp.open("GET", theUrl, true); // true for asynchronous 
+    xmlHttp.send(null);
+}
+
+var handler = function(uri) {
+  console.log("Handler called");
+  console.log(callback)
+  window.callback(uri)
+  hideModal();
+  //window.close()
+}
+
+
+window.open = function(url, callback) {
+	console.info('Got a call to window.open for : ' + url)
+	console.info(callback)
+	console.trace()
+	if(url) {
+		httpGet(url, function(responseText) {
+			showModal({
+				html : responseText
+			})		
+		})		
+	} else {
+		showModal({
+			html : ''
+		});
+	}
+	return fakeWindow;
+	/* Open the prescribed window, but in a modal dialog */
+}
+
+window.addEventListener('click', function(event) {
+	var modal = document.getElementById('myModal');
+    if (event.target == modal) {
+        modal.style.display = "none";
+    }
+});
+
+window.addEventListener('keypress', function(evt){
+    evt = evt || window.event;
+    if (evt.keyCode == 27) {
+        hideModal();
+    }
+});
+
+function get_current_program() {
+      //mods.ui.progname = filename
+      var prog = {modules:{},links:[]}
+      var modules = document.getElementById('modules')
+      //
+      // save modules
+      //
+      for (var c = 0; c < modules.childNodes.length; ++c) {
+         var module = modules.childNodes[c]
+         var idnumber = module.id
+         prog.modules[idnumber] = {
+            definition:module.dataset.definition,
+            top:module.dataset.top,
+            left:module.dataset.left,
+            inputs:{},
+            outputs:{}
+            }
+         }
+      //
+      // save links
+      //
+      var svg = document.getElementById('svg')
+      var links = svg.getElementById('links')
+         for (var l = 0; l < links.childNodes.length; ++l) {
+            var link = links.childNodes[l]
+            var linkid = link.id
+            prog.links.push(linkid)
+            }
+      //
+      // download
+      //
+      var text = JSON.stringify(prog)
+      return text
+  }
+
+  function load_program() {
+  	fabmo.getAppConfig(function(err, cfg) {
+  		if(err) {
+  			return log.error(err);
+  		}
+  		console.log(cfg)
+  		try {
+	  		var p = JSON.parse(cfg.current_program);
+	  		fabModules.prog_load(p);  			
+  		} catch(e) {
+  			console.warn('No program to load.')
+  		}
+  	})
+  }
+
+  function init() {
+	  var btn = document.createElement('button')
+	  btn.appendChild(document.createTextNode('Save...'))
+	  document.body.appendChild(btn)
+
+	  btn.addEventListener('click', function(evt) {
+	  	fabmo.setAppConfig({'current_program' : get_current_program()});
+	  });  	
+  }
+
+  init();
+  load_program();
diff --git a/fabmo/js/fabmo.js b/fabmo/js/fabmo.js
new file mode 100644
index 0000000000000000000000000000000000000000..6e9e3a34f6bdf7b7ae347dc4ee1b7fbb6e915b3a
--- /dev/null
+++ b/fabmo/js/fabmo.js
@@ -0,0 +1,1023 @@
+/**
+ * @module fabmo.js
+ */
+(function (root, factory) {
+   var body = document.getElementsByTagName('body');
+    if (typeof define === 'function' && define.amd) {
+        // AMD
+        define([], factory);
+    } else if (typeof exports === 'object') {
+        // Node, CommonJS-like
+        module.exports = factory();
+    } else {
+        // Browser globals (root is window)
+        root.FabMoDashboard = factory();
+    }
+}(this, function () {
+
+/**
+ * The top-level object representing the dashboard.
+ *
+ * @class FabMoDashboard
+ */
+var FabMoDashboard = function(options) {
+    this.version = '{{FABMO_VERSION}}';
+	this.target = window.parent;
+	this.window = window;
+	this._setOptions(options);
+	this._id = 0;
+	this._handlers = {};
+	this.status = {};
+	this.isReady = false;
+	this._event_listeners = {
+		'status' : [],
+		'job_start' : [],
+		'job_end' : [],
+		'change' : [],
+		'disconnect' : [],
+		'reconnect' : [],
+    	'video_frame' : [],
+      'upload_progress':[]
+	};
+	this._setupMessageListener();
+    // listen for escape key press to quit the engine
+    document.onkeyup = function (e) {
+        if(e.keyCode == 27) {
+            console.warn("ESC key pressed - quitting engine.");
+            this.stop();
+        }
+    }.bind(this);
+
+    if(!this.options.defer) {
+		this.ready();
+	}
+}
+
+FabMoDashboard.prototype._setOptions = function(options) {
+	options = options || {};
+	this.options = {};
+	this.options.defer = options.defer || false;
+}
+
+/**
+ * @method isPresent
+ * @return {boolean} True if running in the actual FabMo dashboard.  False otherwise.
+ */
+FabMoDashboard.prototype.isPresent = function() {
+    try {
+        return window.self !== window.top;
+    } catch (e) {
+        return true;
+    }
+}
+
+FabMoDashboard.prototype._download = function(data, strFileName, strMimeType) {
+	// https://github.com/rndme/download
+	// data can be a string, Blob, File, or dataURL
+
+	var self = window 						// this script is only for browsers anyway...
+	var u = "application/octet-stream" 		// this default mime also triggers iframe downloads
+	var m = strMimeType || u;
+	var x = data;
+	var D = document;
+	var a = D.createElement("a");
+	var z = function(a){return String(a);};
+	var B = (self.Blob || self.MozBlob || self.WebKitBlob || z);
+	var B=B.call ? B.bind(self) : Blob;
+	var fn = strFileName || "download";
+	var blob;
+	var fr;
+
+	blob = x instanceof B ? x : new B([x], {type: m}) ;
+
+	function d2b(u) {
+		var p= u.split(/[:;,]/),
+		t= p[1],
+		dec= p[2] == "base64" ? atob : decodeURIComponent,
+		bin= dec(p.pop()),
+		mx= bin.length,
+		i= 0,
+		uia= new Uint8Array(mx);
+		for(i;i<mx;++i) { uia[i]=bin.charCodeAt(i); }
+		return new B([uia], {type: t});
+	 }
+
+	function saver(url, winMode){
+
+		if ('download' in a) { //html5 A[download]
+			a.href = url;
+			a.setAttribute("download", fn);
+			a.innerHTML = "downloading...";
+			D.body.appendChild(a);
+			setTimeout(function() {
+				a.click();
+				D.body.removeChild(a);
+				if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(a.href);}, 250 );}
+			}, 66);
+			return true;
+		}
+
+		if(typeof safari !=="undefined" ){ // handle non-a[download] safari as best we can:
+			url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);
+			if(!window.open(url)){ // popup blocked, offer direct download:
+				if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
+			}
+			return true;
+		}
+
+		//do iframe dataURL download (old ch+FF):
+		var f = D.createElement("iframe");
+		D.body.appendChild(f);
+
+		if(!winMode){ // force a mime that will download:
+			url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);
+		}
+		f.src=url;
+		setTimeout(function(){ D.body.removeChild(f); }, 333);
+
+	}//end saver
+
+	if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
+		return navigator.msSaveBlob(blob, fn);
+	}
+
+	if(self.URL){ // simple fast and modern way using Blob and URL:
+		saver(self.URL.createObjectURL(blob), true);
+	}else{
+		// handle non-Blob()+non-URL browsers:
+		if(typeof blob === "string" || blob.constructor===z ){
+			try{
+				return saver( "data:" +  m   + ";base64,"  +  self.btoa(blob)  );
+			}catch(y){
+				return saver( "data:" +  m   + "," + encodeURIComponent(blob)  );
+			}
+		}
+
+		// Blob but not URL:
+		fr=new FileReader();
+		fr.onload=function(e){
+			saver(this.result);
+		};
+		fr.readAsDataURL(blob);
+	}
+	return true;
+} // _download
+
+FabMoDashboard.prototype._call = function(name, data, callback) {
+
+	if(this.isPresent()) {
+		message = {"call":name, "data":data}
+		if(callback) {
+			message.id = this._id++;
+			this._handlers[message.id] = callback;
+		}
+		this.target.postMessage(message, '*');
+	} else {
+		this._simulateCall(name, data, callback);
+	}
+}
+
+FabMoDashboard.prototype._simulateCall = function(name, data, callback) {
+    toaster();
+    var toast = document.getElementById('alert-toaster');
+    var text = document.getElementById('alert-text');
+	switch(name) {
+
+		case "submitJob":
+			var files = [];
+			data.jobs.forEach(function(job) {
+				var name = job.filename || job.file.name;
+				this._download(job.file, name, "text/plain");
+				files.push(name);
+			}.bind(this));
+
+			// Data.length
+			if(data.jobs.length === 1) {
+				var msg = "Job Submitted: " + data.jobs[0].filename
+			} else {
+				var msg = data.jobs.length + " Jobs Submitted: " + files.join(',');
+			}
+			text.textContent = msg;
+			showToaster(toast);
+			callback(null, {})
+		break;
+
+		case "runGCode":
+			text.textContent = "GCode sent to tool: " + data;
+		    showToaster(toast);
+		break;
+
+		case "runSBP":
+			text.textContent = "OpenSBP sent to tool: " + data;
+			showToaster(toast);
+		break;
+
+		case "showDRO":
+			text.textContent = "DRO Shown.";
+			showToaster(toast);
+		break;
+
+		case "hideDRO":
+			text.textContent = "DRO Hidden.";
+			showToaster(toaster);
+		break;
+
+		default:
+			text.textContent = name + " called.";
+			showToaster(toast);
+		break;
+	}
+}
+
+FabMoDashboard.prototype._on = function(name, callback) {
+
+	var message = {"on":name}
+	if(callback) {
+		this._event_listeners[name].push(callback);
+	}
+	this.target.postMessage(message, '*');
+}
+
+/**
+ * Bind a callback to the specified event.
+ *
+ * @method on
+ * @param {String} name Event name
+ * @param {function} callback Event handler
+ */
+FabMoDashboard.prototype.on = function(name, callback) {
+	this._on(name, callback);
+}
+
+FabMoDashboard.prototype.off = function(name, callback) {
+	var listeners = this._event_listeners[name] || [];
+	if(!callback) {
+		this._event_listeners[name] = [];
+	} else {
+ 		var idx = listeners.indexOf(5);
+		if (idx > -1) {
+    		this._event_listeners[name].splice(index, 1);
+		}
+	}
+}
+
+FabMoDashboard.prototype._setupMessageListener = function() {
+	this.window.addEventListener('message', function (evt) {
+		var message = evt.data;
+		switch(message.type) {
+			case 'cb':
+				if('id' in message) {
+		 			if(message.id in this._handlers) {
+		 				cb = this._handlers[message.id]
+		 				if(message.status === "success") {
+		 					cb(null, message.data);
+		 				} else {
+		 					cb(message.message, null);
+		 				}
+		 			}
+		 		}
+		 		break;
+
+			case 'evt':
+				if('id' in message) {
+					if(message.id in this._event_listeners) {
+						listeners = this._event_listeners[message.id]
+						for(i in listeners) {
+							listeners[i](message.data);
+						}
+					}
+				}
+				break;
+			}
+	}.bind(this));
+}
+
+/**
+ * Indicate that the app is loaded and ready to go!
+ * @method ready
+ */
+FabMoDashboard.prototype.ready = function() {
+	this._call('ready');
+	this.isReady = true;
+}
+
+/**
+ * Set the message to display while an app is loading.  You can call this any time before
+ * calling the `ready()` function, to indicate loading or setup progress.
+ * @method setBusy
+ * @param {String} The message to display.
+ */
+FabMoDashboard.prototype.setBusyMessage = function(message) {
+	this._call('setBusyMessage', {'message' : message});
+}
+
+/**
+ * If this app was invoked from another app, get the arguments (if any) that were passed on invocation.
+ *
+ * @method getAppArgs
+ * @callback {Object} The arguments passed to this app, or undefined.
+ */
+FabMoDashboard.prototype.getAppArgs = function(callback) {
+	this._call("getAppArgs", null, callback);
+}
+
+/**
+ * Get the information for this app.
+ *
+ * @method getAppInfo
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.info App information
+ * @param {String} callback.info.name The name of the App
+ */
+FabMoDashboard.prototype.getAppInfo = function(callback) {
+	this._call("getAppInfo", null, callback);
+}
+
+/**
+ * Launch the specified app by app ID, with optional arguments.
+ *
+ * @method launchApp
+ * @param {String} id The id of the app to launch.
+ * @param {Object} args Arguments object to pass to the app.
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error
+ * @param {Object} callback.app App info for the launched app if launch was successful
+ */
+FabMoDashboard.prototype.launchApp = function(id, args, callback) {
+	this._call("launchApp", {'id': id, 'args':args}, callback);
+}
+
+/**
+ * Show the DRO (Digital ReadOut) in the dashboard if it is not already shown.
+ *
+ * @method showDRO
+ * @param {function} callback Called once the DRO has been displayed.
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.showDRO = function(callback) {
+	this._call("showDRO", null, callback);
+}
+
+/**
+ * Hide the DRO (Digital ReadOut) in the dashboard if it is not already hidden.
+ *
+ * @method hideDRO
+ * @param {function} callback Called once the DRO has been hidden.
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.hideDRO = function(callback) {
+	this._call("hideDRO", null, callback);
+}
+
+//Modal Functions
+FabMoDashboard.prototype.showModal = function(options, callback) {
+	var callbacks = {
+		"ok" : options.ok,
+		"cancel" : options.cancel
+	}
+	var showModalCallback = function(err, buttonPressed) {
+		if(err) {
+			callback(err);
+		}
+		var f = callbacks[buttonPressed]();
+		if(f) {
+			f();
+		} else {
+			if(callback) {
+				callback();
+			}
+		}
+ 	}
+ 	options.ok = options.ok ? true : false;
+ 	options.cancel = options.cancel ? true : false;
+
+    this._call("openModal", options, showModalCallback);
+}
+
+FabMoDashboard.prototype.hideModal = function(options, callback) {
+    this._call("closeModal", null, callback);
+}
+
+// Footer Functions
+// FabMoDashboard.prototype.showFooter = function(callback) {
+// 	this._call("showFooter", null, callback);
+// }
+
+/**
+ * Show a notification on the dashboard.  Notifications typically show up as toaster message somewhere on the dashboard,
+ * but the dashboard reserves the right to format or even suppress these messages as suits its needs.
+ *
+ * @method notify
+ * @param {String} type Type of message, which can be one of `info`,`warn`,`error`, or `success`
+ * @param {String} message The text to be displayed in the notification.
+ * @param {function} callback Called once the message has been displayed
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.notify = function(type, message, callback) {
+	this._call("notify", {'type':type, 'message':message}, callback);
+}
+
+FabMoDashboard.prototype.hideFooter = function(callback) {
+	this._call("hideFooter", null, callback);
+}
+
+// Notification functions
+FabMoDashboard.prototype.notification = function(type,message,callback) {
+	this._call("notification", {'type':type,'message':message}, callback);
+}
+FabMoDashboard.prototype.notify = FabMoDashboard.prototype.notification;
+
+function _makeFile(obj) {
+	if(window.jQuery && obj instanceof jQuery) {
+		if(obj.is('input:file')) {
+			obj = obj[0];
+		} else {
+			obj = obj.find('input:file')[0];
+		}
+		file = obj.files[0];
+	} else if(obj instanceof HTMLInputElement) {
+		file = obj.files[0];
+	} else if(obj instanceof File || obj instanceof Blob) {
+		file = obj;
+	} else if(typeof obj === "string") {
+		file = new Blob([obj], {'type' : 'text/plain'});
+	} else {
+		throw new Error('Cannot make File object out of ' + obj);
+	}
+	return file;
+}
+
+function _makeApp(obj) {
+	return {file : _makeFile(obj)};
+}
+
+function _makeJob(obj) {
+	var file = null;
+
+	try {
+		file = _makeFile(obj);
+	} catch(e) {}
+
+	if(file) {
+		return {file : file};
+	} else {
+		var job = {};
+		for (var key in obj) {
+  			if (obj.hasOwnProperty(key)) {
+  				if(key === 'file') {
+  					job['file'] = _makeFile(obj.file);
+  				} else {
+  					job[key] = obj[key];
+  				}
+  			}
+		}
+		return job;
+	}
+}
+
+/**
+ * Submit one or more jobs to the dashboard.
+ * @param {Array|Object|jQuery} jobs A single job object, an array containing multiple job objects, or a jQuery object that points to a file type form input, or a form containing a file type input.
+ * @param {Object} [options] Options for job submission
+ * @todo Finish documenting this function
+ */
+FabMoDashboard.prototype.submitJob = function(jobs, options, callback) {
+	var args = {jobs : []};
+
+	if(window.jQuery && jobs instanceof jQuery) {
+		if(jobs.is('input:file')) {
+			jobs = obj[0];
+		} else {
+			jobs = jobs.find('input:file')[0];
+		}
+		var files = jobs.files;
+		if(files.length) {
+			jobs = [];
+			for(var i=0; i<files.length; i++) {
+				jobs.push(files[i]);
+			}
+		}
+	} else {
+		if(!jobs.length) {
+			jobs = [jobs];
+		}
+	}
+
+	for(var i=0; i<jobs.length; i++) {
+		args.jobs.push(_makeJob(jobs[i]));
+	}
+
+	if(typeof options === 'function') {
+		callback = options;
+		options = {};
+	}
+
+	args.options = options || {};
+	this._call("submitJob", args, callback)
+}
+FabMoDashboard.prototype.submitJobs = FabMoDashboard.prototype.submitJob;
+
+FabMoDashboard.prototype.updateOrder = function(data, callback) {
+	this._call("updateOrder", data, callback);
+}
+
+/**
+ * Resubmit a job by its ID.  Resubmitted jobs come in at the back of the job queue.
+ *
+ * @method resubmitJob
+ * @param {Number} id The ID of the job to resubmit
+ * @param {Object} options Job submission options
+ * @param {Object} options Job submission options
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.resubmitJob = function(id, options, callback) {
+  if(typeof options === 'function') {
+    callback = options;
+    options = {};
+  }
+  var args = {
+    id : id,
+    options : options || {}
+  }
+	this._call("resubmitJob", args, callback)
+}
+
+/**
+ * Delete a job (cancels if running, sends to trash otherwise.)
+ *
+ * @method deleteJob
+ * @param {Number} id The ID of the job to delete
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.deleteJob = function(id, callback) {
+	this._call("deleteJob", id, callback)
+}
+
+FabMoDashboard.prototype.cancelJob = FabMoDashboard.prototype.cancelJob;
+
+/**
+ * Get information about a job.  This works for jobs that are pending, currently running, or in the history.
+ *
+ * @method getJobInfo
+ * @param {Number} id The ID of the job to cancel
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.job Information about the requested job.
+ * @param {String} callback.job.name Job name
+ * @param {String} callback.job.description Job description
+ * @param {Number} callback.job.created_at Job creation time (UTC datetime)
+ * @param {Number} callback.job.started_at Job start time (UTC datetime)
+ * @param {Number} callback.job.finished_at Job completion time (UTC datetime)
+ * @param {String} callback.job.state Current state of the job.  One of `pending`,`running`,`finished`,`cancelled` or `failed`
+ */
+FabMoDashboard.prototype.getJobInfo = function(id, callback) {
+	this._call("getJobInfo", id, callback)
+}
+
+/**
+ * Get a list of jobs that are currently pending and running.
+ *
+ * @method getJobsInQueue
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.jobs Object containing both the pending and running jobs
+ * @param {Array} callback.jobs.pending List of pending jobs.  May be empty.
+ * @param {Array} callback.jobs.running List of running jobs.  May be empty.
+ */
+FabMoDashboard.prototype.getJobsInQueue = function(callback) {
+	this._call("getJobsInQueue",null, callback);
+}
+
+/**
+ * Remove all pending jobs from the queue.
+ *
+ * @method clearJobQueue
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.clearJobQueue = function(callback) {
+	this._call("clearJobQueue",null, callback);
+}
+
+/**
+ * Get a list of jobs in the history.
+ *
+ * @method getJobHistory
+ * @param {Object} options
+ * @param {Number} options.start The location in the results to start
+ * @param {Number} options.count The number of jobs to return
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object[]} callback.jobs Array of jobs in the history
+ */
+FabMoDashboard.prototype.getJobHistory = function(options, callback) {
+	this._call("getJobHistory",options, callback);
+}
+
+/**
+ * Run the next job in the job queue.
+ *
+ * @method runNext
+ * @param {Object} options
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.runNext = function(callback) {
+	this._call("runNext",null, callback);
+}
+
+/**
+ * Pause the execution of the current job or operation.  Operation can be resumed.
+ *
+ * @method pause
+ * @param {Object} options
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.pause = function(callback) {
+	this._call("pause",null, callback);
+}
+
+/**
+ * Stop execution of the current job or operation.  Operation cannot be resumed.
+ *
+ * @method stop
+ * @param {Object} options
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.stop = function(callback) {
+	this._call("stop",null, callback);
+}
+
+/**
+ * Resume the current operation of the system is paused.
+ *
+ * @method resume
+ * @param {Object} options
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.resume = function(callback) {
+	this._call("resume",null, callback);
+}
+
+/**
+ * Perform a fixed manual move in a single axis.  (Sometimes called a nudge)
+ *
+ * @method manualMoveFixed
+ * @param {String} axis One of `x`,`y`,`z`,`a`,`b`,`c`
+ * @param {Number} speed Speed in current tool units
+ * @param {distance} distance The distance to move in current units
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.manualMoveFixed = function(axis, speed, distance, callback) {
+	this._call("manualMoveFixed",{"axis":axis, "speed": speed, "dist":distance}, callback);
+}
+
+/**
+ * Start performing a manual move of the specified axis at the specified speed.
+ *
+ * @method manualStart
+ * @param {Number} axis One of `x`,`y`,`z`,`a`,`b`,`c`
+ * @param {Number} speed Speed in current tool units.  Negative to move in the negative direction.
+ */
+FabMoDashboard.prototype.manualStart = function(axis, speed) {
+	this._call("manualStart",{"axis":axis, "speed":speed}, callback);
+}
+
+/**
+ * Send a "heartbeat" to the system, authorizing continued manual movement.  Manual moves must be continually
+ * refreshed with this heartbeat function, or the tool will stop moving.
+ *
+ * @method manualHeartbeat
+ */
+FabMoDashboard.prototype.manualHeartbeat = function() {
+	this._call("manualHeartbeat",{}, callback);
+}
+
+/**
+ * Stop the tool immediately.
+ *
+ * @method manualStop
+ */
+FabMoDashboard.prototype.manualStop = function() {
+	this._call("manualStop",{}, callback);
+}
+
+/**
+ * Get the list of all the installed apps.
+ * @method getApps
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.apps List of app objects representing all installed apps.
+ */
+FabMoDashboard.prototype.getApps = function(callback) {
+	this._call("getApps",null,callback);
+}
+
+
+FabMoDashboard.prototype.submitApp = function(apps, options, callback) {
+	var args = {apps : []};
+
+	if(window.jQuery && apps instanceof jQuery) {
+		if(apps.is('input:file')) {
+			apps = apps[0];
+		} else {
+			apps = apps.find('input:file')[0];
+		}
+		var files = apps.files;
+		if(files.length) {
+			apps = [];
+			for(var i=0; i<files.length; i++) {
+				apps.push(files[i]);
+			}
+		}
+	} else {
+		if(!apps.length) {
+			apps = [apps];
+		}
+	}
+
+	for(var i=0; i<apps.length; i++) {
+		args.apps.push(_makeApp(apps[i]));
+	}
+
+	if(typeof options === 'function') {
+		callback = options;
+		options = {};
+	}
+
+	args.options = options || {};
+	this._call("submitApp", args, callback)
+}
+FabMoDashboard.prototype.getUpdaterConfig = function(callback) {
+	this._call("getUpdaterConfig", null, callback);
+}
+
+FabMoDashboard.prototype.setUpdaterConfig = function(data, callback) {
+	this._call("setUpdaterConfig", data, callback);
+}
+
+FabMoDashboard.prototype.getInfo = function(callback) {
+	this._call("getInfo", null, callback);
+}
+
+FabMoDashboard.prototype.getConfig = function(callback) {
+	this._call("getConfig", null, callback);
+}
+
+FabMoDashboard.prototype.setConfig = function(data, callback) {
+	this._call("setConfig", data, callback);
+}
+
+FabMoDashboard.prototype.deleteApp = function(id, callback) {
+	this._call("deleteApp",id,callback);
+}
+
+FabMoDashboard.prototype.runGCode = function(text, callback) {
+	this._call("runGCode", text, callback);
+}
+
+FabMoDashboard.prototype.runSBP = function(text, callback) {
+	this._call("runSBP", text, callback);
+}
+
+FabMoDashboard.prototype.connectToWifi = function(ssid, key, callback) {
+	this._call("connectToWifi", {'ssid':ssid, 'key':key}, callback);
+}
+
+FabMoDashboard.prototype.disconnectFromWifi = function(callback) {
+	this._call("disconnectFromWifi", null, callback);
+}
+
+FabMoDashboard.prototype.forgetWifi = function(ssid, key, callback) {
+	this._call("forgetWifi", {'ssid':ssid}, callback);
+}
+
+FabMoDashboard.prototype.enableWifi = function(callback) {
+	this._call("enableWifi", null, callback);
+}
+
+FabMoDashboard.prototype.disableWifi = function(callback) {
+	this._call("disableWifi", null, callback);
+}
+
+FabMoDashboard.prototype.enableWifiHotspot = function(callback) {
+	this._call("enableWifiHotspot", null, callback);
+}
+
+FabMoDashboard.prototype.disableWifiHotspot = function(callback) {
+	this._call("disableWifiHotspot", null, callback);
+}
+
+FabMoDashboard.prototype.getWifiNetworks = function(callback) {
+	this._call("getWifiNetworks", null, callback);
+}
+
+FabMoDashboard.prototype.getWifiNetworkHistory = function(callback) {
+	this._call("getWifiNetworkHistory", null, callback);
+}
+
+FabMoDashboard.prototype.getNetworkIdentity = function(callback) {
+	this._call("getNetworkIdentity", null, callback);
+}
+
+FabMoDashboard.prototype.setNetworkIdentity = function(identity, callback) {
+	this._call("setNetworkIdentity", identity, callback);
+}
+
+FabMoDashboard.prototype.isOnline = function(callback) {
+	this._call("isOnline", null, callback);
+}
+
+/**
+ * Get a list of all the macros installed on the tool.
+ *
+ * @method getMacros
+ * @param callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.macros List of macro objects currently installed.
+ */
+FabMoDashboard.prototype.getMacros = function(callback) {
+	this._call("getMacros", null, callback);
+}
+
+/**
+ * Run the specified macro immediately.  Macro does not appear in the job history.
+ *
+ * @method runMacro
+ * @param {Number} id The id of the macro to run.
+ * @param callback
+ * @param {Error} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.runMacro = function(id, callback) {
+	this._call("runMacro", id, callback);
+}
+
+FabMoDashboard.prototype.updateMacro = function(id, macro, callback) {
+	this._call("updateMacro", {'id':id, 'macro':macro}, callback);
+}
+
+/**
+ * Request a status report from the system.  The status object is returned in the callback to this function, as well as posted
+ * with the status event.  To recieve updates to system status as it changes, you should bind a handler to the status event,
+ * and _not_ poll using `requestStatus`.
+ *
+ * @method requestStatus
+ * @param {function} callback
+ * @param {Object} callback.status The status report object
+ * @param {String} callback.status.state The system state, one of `idle`,`running`,`paused`,`stopped`,`manual` or `dead`
+ * @param {Number} callback.status.posx The current x-axis position
+ * @param {Number} callback.status.posy The current y-axis position
+ * @param {Number} callback.status.posz The current z-axis position
+ * @param {Number} callback.status.posa The current a-axis position
+ * @param {Number} callback.status.posb The current b-axis position
+ * @param {Number} callback.status.posc The current c-axis position
+ * @param {Number} callback.status.in1 The current state of input 1 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 2 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 3 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 4 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 5 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 6 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 7 (`0` or `1`)
+ * @param {Number} callback.status.in1 The current state of input 8 (`0` or `1`)
+ * @param {String} callback.status.unit The current tool units (either `in` or `mm`)
+ * @param {Number} callback.status.line The line number in the currently running file (if a file is running)
+ * @param {Number} callback.status.nb_lines The total number of lines in the currently running file (if a file is running)
+ * @param {boolean} callback.status.auth True if the tool is currently authorized for movement
+ * @param {Object} callback.status.current_file Object describing the currently running file (or null if not running a file)
+ * @param {Object} callback.status.job Object describing the currently running job (or null if not running a job)
+ */
+FabMoDashboard.prototype.requestStatus = function(callback) {
+	this._call("requestStatus", null, callback);
+}
+
+/**
+ * Start a connection to the video streaming service on your fabmo device.
+ * A video_frame event will be triggered each time a new frame is send to the browser.
+ *
+ * @method startVideoStreaming
+ * @param {function} callback
+ * @param {Object} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.startVideoStreaming = function(callback) {
+	this._call("startVideoStreaming", null, callback);
+}
+
+
+/**
+ * Add a listener to the video frame event.
+ * This will return an Image object, ready to be displayed on a canvas
+ * @method startVideoStreaming
+ * @param {function} callback
+ * @param {Object} callback.err Error object if there was an error.
+ */
+FabMoDashboard.prototype.onVideoFrame = function (callback) {
+  this.on('video_frame',function(data){
+    imageObj = new Image();
+    imageObj.src = "data:image/jpeg;base64,"+data;
+    imageObj.onload = function(){
+      callback(imageObj);
+    }
+  })
+};
+
+FabMoDashboard.prototype.deleteMacro = function(id, callback) {
+	this._call("deleteMacro", id, callback);
+}
+
+/**
+ * Get the configuration object for the currently running app.  The configuration object is a JSON object
+ * of no specific description that is saved with each app.  It can be used to store app-specific configuration data.
+ *
+ * @method getAppConfig
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.config Configuration object or {} if no configuration has been saved in the app.
+ */
+FabMoDashboard.prototype.getAppConfig = function(callback) {
+	this._call("getAppConfig", null, callback);
+}
+
+/**
+ * Set the app configuration to the provided option.
+ *
+ * @method setAppConfig
+ * @param {Object} config The configuration object to set.
+ * @param {function} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.config Configuration object or {} if no configuration has been saved in the app.
+ */
+FabMoDashboard.prototype.setAppConfig = function(config, callback) {
+	this._call("setAppConfig", config, callback);
+}
+
+/**
+ * Get the current FabMo version
+ *
+ * @method getVersion
+ * @param {fuction} callback
+ * @param {Error} callback.err Error object if there was an error.
+ * @param {Object} callback.version Object describing the fabmo software version.
+ * @param {String} callback.version.hash The git hash of the currently running FabMo software
+ * @param {String} callback.version.number The user-friendly release version number (or empty string if not a released version
+ * @param {boolean} callback.version.debug True if FabMo is running in "debug mode"
+ * @param {String} callback.version.type The type of FabMo software build.  `dev` for development or `release` for released build.
+ */
+FabMoDashboard.prototype.getVersion = function(callback) {
+	this._call("getVersion", null, callback);
+}
+
+/**
+ * Control top level navigation for the dashboard.  Usually this is used to open another browser/tab window with a link from within an app,
+ * or more rarely, to navigate away from the dashboard.
+ * @method navigate
+ * @param {String} url The URL to open
+ * @param {Object} options Options for top-level navigation.
+ * @param {String} options.target The link target (same as the target argument of `window.open`)
+ */
+FabMoDashboard.prototype.navigate = function(url, options, callback) {
+	var loc = window.location.href;
+	this._call("navigate", {'path' : loc.substr(0, loc.lastIndexOf('/')) + '/', 'url' : url, 'options' : options || {}}, callback);
+}
+
+FabMoDashboard.prototype.getCurrentUser = function(callback){
+  this._call("getCurrentUser",null,callback);
+}
+FabMoDashboard.prototype.addUser = function(user_info,callback){
+  this._call("addUser",user_info,callback);
+}
+FabMoDashboard.prototype.modifyUser = function(user,callback){
+  this._call("modifyUser",user,callback);
+}
+FabMoDashboard.prototype.deleteUser = function(user,callback){
+  this._call("deleteUser",user,callback);
+}
+FabMoDashboard.prototype.getUsers = function(callback){
+  this._call("getUsers",null,callback);
+}
+
+FabMoDashboard.prototype.getUpdaterStatus = function(callback){
+  this._call("getUpdaterStatus",null,callback);
+}
+
+
+var toaster = function () {
+	var el = document.createElement('div');
+    el.setAttribute('id', 'alert-toaster');
+    el.style.cssText = 'position:fixed; visibility:hidden; margin: auto; top: 20px; right: 20px; width: 250px; height: 60px; background-color: #F3F3F3; border-radius: 3px; z-index: 1005; box-shadow: 4px 4px 7px -2px rgba(0,0,0,0.75);';
+    el.innerHTML = "<span id='alert-text' style= 'position:absolute; margin: auto; top: 0; right: 0; bottom: 0; left: 0; height: 20px; width: 250px; text-align: center;'></span>";
+	document.body.appendChild(el);
+}
+var showToaster = function (toaster) {
+    toaster.style.visibility = 'visible';
+    setTimeout(function(){document.body.removeChild(toaster)}, 1000);
+}
+return FabMoDashboard;
+
+}));
diff --git a/fabmo/package.json b/fabmo/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..224efa5a1405ca8327c0f60667819c989d88083e
--- /dev/null
+++ b/fabmo/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "Mods",
+  "license": "MIT",
+  "main": "./index.html",
+  "icon": "icon.png",
+  "icon_color": "#ffffff",
+  "version": "0.0.1",
+  "author": "Ryan Sturmer",
+  "description": "MIT CBA's FabModules for FabMo"
+}
diff --git a/files.html b/files.html
index 61a4d512d33810469564ac40fe10ff3560b874a6..25fca1112730e19dd6cff43d9e16db3ffa2d4a2b 100644
--- a/files.html
+++ b/files.html
@@ -8,9 +8,24 @@
       window.close()
       }
    </script>
-   <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.git</i><br>
+   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./.DS_Store'>.DS_Store</a><br>
+<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.git</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./.gitignore'>.gitignore</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./README.md'>README.md</a><br>
+<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fabmo</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/.DS_Store'>.DS_Store</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/Makefile'>Makefile</a><br>
+<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;build</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/build/fabmo-mods-app.fma'>fabmo-mods-app.fma</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/build/icon.png'>icon.png</a><br>
+<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;css</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/css/style.css'>style.css</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/icon.png'>icon.png</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/index.html'>index.html</a><br>
+<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;js</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/js/fabmo-mods.js'>fabmo-mods.js</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/js/fabmo.js'>fabmo.js</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./fabmo/package.json'>package.json</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./files.html'>files.html</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./index.html'>index.html</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;js</i><br>
@@ -26,6 +41,7 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/udpserver.js'>udpserver.js</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./make'>make</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;modules</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/.DS_Store'>.DS_Store</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apa</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/apa/node'>node</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;character</i><br>
@@ -45,6 +61,9 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/event/delay'>delay</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/event/generate'>generate</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/event/pause'>pause</a><br>
+<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fabmo</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/fabmo/status'>status</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/fabmo/submit%20job'>submit job</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;file</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./modules/file/save'>save</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;graph</i><br>
diff --git a/js/mods.js b/js/mods.js
index 6b5d12ee0aa7b455ed06662b8999b812787fd0d6..d9960cffa60a7fa94322ce85c722d90191e12e30 100644
--- a/js/mods.js
+++ b/js/mods.js
@@ -12,7 +12,9 @@
 //
 // closure
 //
-(function(){
+var fabModules;
+
+(function() {
 //
 // globals
 //
@@ -28,6 +30,7 @@ mods.ui = {source:null,
    link_highlight:'rgb(255,0,0)'
    }
 mods.globals = {}
+fabModules = mods;
 //
 // set up UI
 //
@@ -1379,4 +1382,9 @@ function window_touchup(evt) {
       window.removeEventListener('touchmove',window_touchmove)
       window.removeEventListener('touchend',window_touchup)
    }
-})()
+
+// Things to pass into the global fabModules object
+mods.prog_load = prog_load;
+
+}
+)()
diff --git a/modules/fabmo/status b/modules/fabmo/status
new file mode 100755
index 0000000000000000000000000000000000000000..821ee7cc25d35ed2e5faa85d560c5ceddcaebc68
--- /dev/null
+++ b/modules/fabmo/status
@@ -0,0 +1,74 @@
+//
+// ui button
+//
+// Neil Gershenfeld 
+// (c) Massachusetts Institute of Technology 2015,6
+// 
+// This work may be reproduced, modified, distributed, performed, and 
+// displayed for any purpose, but must acknowledge the mods
+// project. Copyright is retained and must be preserved. The work is 
+// provided as is; no warranty is provided, and users accept all 
+// liability.
+//
+// closure
+//
+(function(){
+//
+// module globals
+//
+var mod = {
+   count : 0
+}
+//
+// name
+//
+var name = 'Status Report'
+//
+// initialization
+//
+var init = function() {
+   fabmo.on('status', function(stat) {
+      console.log(outputs)
+      mod.count++;
+      outputs.status.event(stat)
+   });
+   fabmo.requestStatus();
+}
+//
+// inputs
+//
+var inputs = {
+   }
+//
+// outputs
+//
+var outputs = {
+   status:{type:"Object",
+      label:"Status",
+      event:function(obj){
+         mod.content.innerHTML = "Status Reports:<br/>" + mod.count;
+         mods.output(mod,"status",obj)}
+      }
+   }
+//
+// interface
+//
+var interface = function(div){
+   mod.div = div 
+   mod.content = document.createElement('div')
+   mod.content.innerHTML = 'Status Reports:<br/>0'
+   //mod.content.style.padding = mods.ui.padding
+   //mod.content.style.margin = 1
+   div.appendChild(mod.content)
+}
+//
+// return values
+//
+return ({
+   name:name,
+   init:init,
+   inputs:inputs,
+   outputs:outputs,
+   interface:interface
+   })
+}())
diff --git a/modules/fabmo/submit job b/modules/fabmo/submit job
new file mode 100644
index 0000000000000000000000000000000000000000..1fc963884c3ca21c80d661b0ce68052274dc4166
--- /dev/null
+++ b/modules/fabmo/submit job	
@@ -0,0 +1,95 @@
+//
+// save file
+//
+// Neil Gershenfeld 
+// (c) Massachusetts Institute of Technology 2016
+// 
+// This work may be reproduced, modified, distributed, performed, and 
+// displayed for any purpose, but must acknowledge the mods
+// project. Copyright is retained and must be preserved. The work is 
+// provided as is; no warranty is provided, and users accept all 
+// liability.
+//
+// closure
+//
+(function(){
+//
+// module globals
+//
+var mod = {}
+//
+// name
+//
+var name = 'fabmo submit job'
+//
+// initialization
+//
+var init = function() {
+   }
+//
+// inputs
+//
+var inputs = {
+   file:{type:'object',
+      event:function(evt){
+         mod.name = evt.detail.name
+         mod.contents = evt.detail.contents
+         submit_job()
+         }}}
+//
+// outputs
+//
+var outputs = {}
+//
+// interface
+//
+var interface = function(div){
+   mod.div = div
+   //
+   // info
+   //
+   var text = document.createTextNode('name:')
+      div.appendChild(text)
+      mod.nametext = text
+   div.appendChild(document.createElement('br'))
+   var text = document.createTextNode('size:')
+      div.appendChild(text)
+      mod.sizetext = text
+   div.appendChild(document.createElement('br'))
+   }
+//
+// local functions
+//
+function submit_job() {
+/*   var a = document.createElement('a')
+   a.setAttribute('href','data:text/plain;charset=utf-8,'+ encodeURIComponent(mod.contents))
+*/
+   var code = mod.contents
+   console.log(code);
+   fabmo.submitJob({
+      file : code,
+      filename : mod.name
+    }, { stayHere : true });
+/*
+   a.setAttribute('download',mod.namea)
+   a.style.display = 'none'
+   document.body.appendChild(a)
+   a.click()
+   document.body.removeChild(a)
+   */
+   mod.nametext.nodeValue = 'name: '+mod.name
+   mods.fit(mod.div)
+   mod.sizetext.nodeValue = 'size: '+mod.contents.length
+   mods.fit(mod.div)
+   }
+//
+// return values
+//
+return ({
+   name:name,
+   init:init,
+   inputs:inputs,
+   outputs:outputs,
+   interface:interface
+   })
+}())
diff --git a/modules/index.html b/modules/index.html
index b31f0ff962b4b6b107cc4c02865331fbd6c95341..75a1cceb01cc3be8807e0efc89b0450c44a1ade0 100644
--- a/modules/index.html
+++ b/modules/index.html
@@ -28,6 +28,9 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/event/delay')">delay</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/event/generate')">generate</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/event/pause')">pause</a><br>
+<i>fabmo</i><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/fabmo/status')">status</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/fabmo/submit%20job')">submit job</a><br>
 <i>file</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:handler('modules/file/save')">save</a><br>
 <i>graph</i><br>