azkaban-aplcache

Changes

build.xml 48(+19 -29)

scheduleTriggerMigration/file2Trigger/file2Trigger 5(+0 -5)

scheduleTriggerMigration/file2Trigger/lib/azkaban-2.2.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/commons-dbcp-1.4.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/commons-dbutils-1.5.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/commons-jexl-2.1.1.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/commons-logging-1.1.1.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/commons-pool-1.6.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/file2trigger.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/jackson-core-asl-1.9.5.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/jackson-mapper-asl-1.9.5.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/joda-time-2.0.jar 0(+0 -0)

scheduleTriggerMigration/file2Trigger/lib/log4j-1.2.16.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/azkaban-2.1.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/commons-dbcp-1.4.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/commons-dbutils-1.5.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/commons-io-2.4.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/commons-pool-1.6.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/jackson-core-asl-1.9.5.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/jackson-mapper-asl-1.9.5.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/joda-time-2.0.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/log4j-1.2.16.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/lib/schedule2file.jar 0(+0 -0)

scheduleTriggerMigration/schedule2File/schedule2file 7(+0 -7)

src/java/azkaban/utils/LogSummary.java 349(+0 -349)

src/less/base.less 16(+16 -0)

src/less/flow.less 36(+27 -9)

src/less/Makefile 16(+16 -0)

src/tl/flowstats.tl 133(+133 -0)

src/tl/flowsummary.tl 160(+59 -101)

src/tl/Makefile 17(+17 -0)

src/web/js/azkaban.jobdetails.view.js 341(+0 -341)

src/web/js/dust-core-2.2.2.min.js 9(+0 -9)

Details

build.xml 48(+19 -29)

diff --git a/build.xml b/build.xml
index c695082..e1f77bb 100644
--- a/build.xml
+++ b/build.xml
@@ -41,12 +41,20 @@
 	<!-- set the build number based on environment variable, otherwise blank -->
 	<property environment="env" description="System environment variables (including those set by Hudson)" />
 
-	<target name="all" depends="clean, jars" description="Builds all jars" />
+	<target name="all" depends="clean, package" description="Builds jars and packages." />
 
 	<target name="clean" description="Delete generated files.">
 		<echo message="Deleting generated files in dist" />
 		<delete dir="${dist.jar.dir}" />
 		<delete dir="${dist.classes.dir}" />
+		<delete dir="${dist.dust.dir}" />
+		<delete dir="${dist.less.dir}" />
+    <exec dir="${dust.src.dir}" executable="make" failonerror="true">
+      <arg value="clean" />
+    </exec>
+    <exec dir="${less.src.dir}" executable="make" failonerror="true">
+      <arg value="clean" />
+    </exec>
 	</target>
 
 	<target name="build" description="Compile main source tree java files">
@@ -70,32 +78,17 @@
 			<classpath refid="main.classpath" />
 		</javac>
 
-		<!-- Compile dustjs templates -->
-		<!-- Note: Because apply does not support multiple srcfile and targetfile
-				 elements, and for and foreach requires ant-contrib, we use targetfile 
-				 for the template name parameter and then redirect the output of dustc
-				 to the final output file -->
-		<echo message="Compiling Dust templates." />
-		<apply dir="${dust.src.dir}" executable="dustc" relative="true">
-			<mapper type="glob" from="*.tl" to="*" />
-			<targetfile prefix="--name=" />
-			<srcfile />
-			<fileset dir="${dust.src.dir}" includes="*.tl" />
-			<redirector>
-				<outputmapper id="out" type="glob" from="*.tl" to="${dist.dust.dir}/*.js" />
-			</redirector>
-		</apply>
+    <!-- Compile dustjs templates -->
+    <exec dir="${dust.src.dir}" executable="make" failonerror="true"/>
+		<copy todir="${dist.dust.dir}">
+      <fileset dir="${dust.src.dir}/obj" includes="*.js" />
+		</copy>
 
 		<!-- Compile LESS to CSS -->
-		<echo message="Compiling LESS style sheets." />
-		<apply dir="${less.src.dir}" executable="lessc" relative="true">
-			<mapper type="glob" from="*.less" to="*.css" />
-			<srcfile />
-			<fileset dir="${less.src.dir}" includes="*.less" />
-			<redirector>
-				<outputmapper id="out" type="glob" from="*.less" to="${dist.less.dir}/*.css" />
-			</redirector>
-		</apply>
+    <exec dir="${less.src.dir}" executable="make" failonerror="true"/>
+		<copy todir="${dist.less.dir}" >
+      <fileset dir="${less.src.dir}/obj" includes="*.css" />
+    </copy>
 	</target>
 	
 	<target name="jars" depends="build" description="Create azkaban jar">
@@ -293,10 +286,7 @@
 		
 		<!-- Copy compiled less CSS -->
 		<copy todir="${dist.solo.package.dir}/web/css">
-      <fileset dir="${dist.less.dir}">
-        <include name="azkaban.css" />
-        <include name="azkaban-svg.css" />
-      </fileset>
+      <fileset dir="${dist.less.dir}" />
 		</copy>
 		
 		<!-- Copy sql files -->
diff --git a/src/java/azkaban/executor/ExecutorManager.java b/src/java/azkaban/executor/ExecutorManager.java
index bf87720..06af3b7 100644
--- a/src/java/azkaban/executor/ExecutorManager.java
+++ b/src/java/azkaban/executor/ExecutorManager.java
@@ -200,26 +200,42 @@ public class ExecutorManager extends EventHandler implements ExecutorManagerAdap
 	}
 	
 	@Override
-	public List<ExecutableFlow> getExecutableFlows(Project project, String flowId, int skip, int size) throws ExecutorManagerException {
-		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(project.getId(), flowId, skip, size);
+	public List<ExecutableFlow> getExecutableFlows(
+      Project project, String flowId, int skip, int size) 
+      throws ExecutorManagerException {
+		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(
+        project.getId(), flowId, skip, size);
 		return flows;
 	}
 	
 	@Override
-	public List<ExecutableFlow> getExecutableFlows(int skip, int size) throws ExecutorManagerException {
+	public List<ExecutableFlow> getExecutableFlows(int skip, int size) 
+      throws ExecutorManagerException {
 		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(skip, size);
 		return flows;
 	}
 	
 	@Override
-	public List<ExecutableFlow> getExecutableFlows(String flowIdContains, int skip, int size) throws ExecutorManagerException {
-		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(null, '%'+flowIdContains+'%', null, 0, -1, -1 , skip, size);
+	public List<ExecutableFlow> getExecutableFlows(
+      String flowIdContains, int skip, int size) 
+      throws ExecutorManagerException {
+		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(
+        null, '%'+flowIdContains+'%', null, 0, -1, -1 , skip, size);
 		return flows;
 	}
 
 	@Override
-	public List<ExecutableFlow> getExecutableFlows(String projContain, String flowContain, String userContain, int status, long begin, long end, int skip, int size) throws ExecutorManagerException {
-		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(projContain, flowContain, userContain, status, begin, end , skip, size);
+	public List<ExecutableFlow> getExecutableFlows(
+      String projContain, 
+      String flowContain, 
+      String userContain, 
+      int status, 
+      long begin, 
+      long end, 
+      int skip, 
+      int size) throws ExecutorManagerException {
+		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(
+        projContain, flowContain, userContain, status, begin, end , skip, size);
 		return flows;
 	}
 	
@@ -1075,15 +1091,24 @@ public class ExecutorManager extends EventHandler implements ExecutorManagerAdap
 	}
 	
 	@Override
-	public int getExecutableFlows(int projectId, String flowId, int from, int length, List<ExecutableFlow> outputList) throws ExecutorManagerException {
-		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(projectId, flowId, from, length);
+	public int getExecutableFlows(
+      int projectId, 
+      String flowId, 
+      int from, 
+      int length, 
+      List<ExecutableFlow> outputList) throws ExecutorManagerException {
+		List<ExecutableFlow> flows = executorLoader.fetchFlowHistory(
+        projectId, flowId, from, length);
 		outputList.addAll(flows);
 		return executorLoader.fetchNumExecutableFlows(projectId, flowId);
 	}
 
 	@Override
-	public List<ExecutableFlow> getExecutableFlows(int projectId, String flowId, int from, int length, Status status) throws ExecutorManagerException {
-		return executorLoader.fetchFlowHistory(projectId, flowId, from, length, status);
+	public List<ExecutableFlow> getExecutableFlows(
+      int projectId, String flowId, int from, int length, Status status) 
+      throws ExecutorManagerException {
+		return executorLoader.fetchFlowHistory(
+        projectId, flowId, from, length, status);
 	}
 
 	/* 
diff --git a/src/java/azkaban/executor/JdbcExecutorLoader.java b/src/java/azkaban/executor/JdbcExecutorLoader.java
index 5373b7d..6d44b57 100644
--- a/src/java/azkaban/executor/JdbcExecutorLoader.java
+++ b/src/java/azkaban/executor/JdbcExecutorLoader.java
@@ -180,12 +180,14 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 	}
 	
 	@Override
-	public int fetchNumExecutableFlows(int projectId, String flowId) throws ExecutorManagerException {
+	public int fetchNumExecutableFlows(int projectId, String flowId) 
+			throws ExecutorManagerException {
 		QueryRunner runner = createQueryRunner();
 		
 		IntHandler intHandler = new IntHandler();
 		try {
-			int count = runner.query(IntHandler.NUM_FLOW_EXECUTIONS, intHandler, projectId, flowId);
+			int count = runner.query(
+					IntHandler.NUM_FLOW_EXECUTIONS, intHandler, projectId, flowId);
 			return count;
 		} catch (SQLException e) {
 			throw new ExecutorManagerException("Error fetching num executions", e);
@@ -193,12 +195,14 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 	}
 	
 	@Override
-	public int fetchNumExecutableNodes(int projectId, String jobId) throws ExecutorManagerException {
+	public int fetchNumExecutableNodes(int projectId, String jobId) 
+			throws ExecutorManagerException {
 		QueryRunner runner = createQueryRunner();
 		
 		IntHandler intHandler = new IntHandler();
 		try {
-			int count = runner.query(IntHandler.NUM_JOB_EXECUTIONS, intHandler, projectId, jobId);
+			int count = runner.query(
+					IntHandler.NUM_JOB_EXECUTIONS, intHandler, projectId, jobId);
 			return count;
 		} catch (SQLException e) {
 			throw new ExecutorManagerException("Error fetching num executions", e);
@@ -206,12 +210,19 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 	}
 	
 	@Override
-	public List<ExecutableFlow> fetchFlowHistory(int projectId, String flowId, int skip, int num) throws ExecutorManagerException {
+	public List<ExecutableFlow> fetchFlowHistory(int projectId, String flowId, 
+			int skip, int num) throws ExecutorManagerException {
 		QueryRunner runner = createQueryRunner();
 		FetchExecutableFlows flowHandler = new FetchExecutableFlows();
 
 		try {
-			List<ExecutableFlow> properties = runner.query(FetchExecutableFlows.FETCH_EXECUTABLE_FLOW_HISTORY, flowHandler, projectId, flowId, skip, num);
+			List<ExecutableFlow> properties = runner.query(
+					FetchExecutableFlows.FETCH_EXECUTABLE_FLOW_HISTORY, 
+					flowHandler, 
+					projectId, 
+					flowId, 
+					skip, 
+					num);
 			return properties;
 		} catch (SQLException e) {
 			throw new ExecutorManagerException("Error fetching active flows", e);
@@ -219,12 +230,21 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 	}
 	
 	@Override
-	public List<ExecutableFlow> fetchFlowHistory(int projectId, String flowId, int skip, int num, Status status) throws ExecutorManagerException {
+	public List<ExecutableFlow> fetchFlowHistory(
+			int projectId, String flowId, int skip, int num, Status status) 
+			throws ExecutorManagerException {
 		QueryRunner runner = createQueryRunner();
 		FetchExecutableFlows flowHandler = new FetchExecutableFlows();
 
 		try {
-			List<ExecutableFlow> properties = runner.query(FetchExecutableFlows.FETCH_EXECUTABLE_FLOW_BY_STATUS, flowHandler, projectId, flowId, status.getNumVal(), skip, num);
+			List<ExecutableFlow> properties = runner.query(
+					FetchExecutableFlows.FETCH_EXECUTABLE_FLOW_BY_STATUS, 
+					flowHandler, 
+					projectId, 
+					flowId, 
+					status.getNumVal(), 
+					skip, 
+					num);
 			return properties;
 		} catch (SQLException e) {
 			throw new ExecutorManagerException("Error fetching active flows", e);
@@ -232,13 +252,18 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 	}
 	
 	@Override
-	public List<ExecutableFlow> fetchFlowHistory(int skip, int num) throws ExecutorManagerException {
+	public List<ExecutableFlow> fetchFlowHistory(int skip, int num) 
+			throws ExecutorManagerException {
 		QueryRunner runner = createQueryRunner();
 
 		FetchExecutableFlows flowHandler = new FetchExecutableFlows();
 		
 		try {
-			List<ExecutableFlow> properties = runner.query(FetchExecutableFlows.FETCH_ALL_EXECUTABLE_FLOW_HISTORY, flowHandler, skip, num);
+			List<ExecutableFlow> properties = runner.query(
+					FetchExecutableFlows.FETCH_ALL_EXECUTABLE_FLOW_HISTORY, 
+					flowHandler, 
+					skip, 
+					num);
 			return properties;
 		} catch (SQLException e) {
 			throw new ExecutorManagerException("Error fetching active flows", e);
@@ -247,7 +272,15 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 	
 
 	@Override
-	public List<ExecutableFlow> fetchFlowHistory(String projContain, String flowContains, String userNameContains, int status, long startTime, long endTime, int skip, int num) throws ExecutorManagerException {
+	public List<ExecutableFlow> fetchFlowHistory(
+			String projContain, 
+			String flowContains, 
+			String userNameContains, 
+			int status, 
+			long startTime, 
+			long endTime, 
+			int skip, 
+			int num) throws ExecutorManagerException {
 		String query = FetchExecutableFlows.FETCH_BASE_EXECUTABLE_FLOW_QUERY;
 		ArrayList<Object> params = new ArrayList<Object>();
 		
@@ -329,7 +362,8 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 		FetchExecutableFlows flowHandler = new FetchExecutableFlows();
 
 		try {
-			List<ExecutableFlow> properties = runner.query(query, flowHandler, params.toArray());
+			List<ExecutableFlow> properties = runner.query(
+					query, flowHandler, params.toArray());
 			return properties;
 		} catch (SQLException e) {
 			throw new ExecutorManagerException("Error fetching active flows", e);
@@ -839,13 +873,28 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 		
 	}
 	
-	private static class FetchExecutableFlows implements ResultSetHandler<List<ExecutableFlow>> {
-		private static String FETCH_BASE_EXECUTABLE_FLOW_QUERY = "SELECT exec_id, enc_type, flow_data FROM execution_flows ";
-		private static String FETCH_EXECUTABLE_FLOW = "SELECT exec_id, enc_type, flow_data FROM execution_flows WHERE exec_id=?";
-		//private static String FETCH_ACTIVE_EXECUTABLE_FLOW = "SELECT ex.exec_id exec_id, ex.enc_type enc_type, ex.flow_data flow_data FROM execution_flows ex INNER JOIN active_executing_flows ax ON ex.exec_id = ax.exec_id";
-		private static String FETCH_ALL_EXECUTABLE_FLOW_HISTORY = "SELECT exec_id, enc_type, flow_data FROM execution_flows ORDER BY exec_id DESC LIMIT ?, ?";
-		private static String FETCH_EXECUTABLE_FLOW_HISTORY = "SELECT exec_id, enc_type, flow_data FROM execution_flows WHERE project_id=? AND flow_id=? ORDER BY exec_id DESC LIMIT ?, ?";
-		private static String FETCH_EXECUTABLE_FLOW_BY_STATUS = "SELECT exec_id, enc_type, flow_data FROM execution_flows WHERE project_id=? AND flow_id=? AND status=? ORDER BY exec_id DESC LIMIT ?, ?";
+	private static class FetchExecutableFlows 
+			implements ResultSetHandler<List<ExecutableFlow>> {
+		private static String FETCH_BASE_EXECUTABLE_FLOW_QUERY = 
+				"SELECT exec_id, enc_type, flow_data FROM execution_flows ";
+		private static String FETCH_EXECUTABLE_FLOW = 
+				"SELECT exec_id, enc_type, flow_data FROM execution_flows " +
+						"WHERE exec_id=?";
+		//private static String FETCH_ACTIVE_EXECUTABLE_FLOW = 
+		//	"SELECT ex.exec_id exec_id, ex.enc_type enc_type, ex.flow_data flow_data " +
+		//			"FROM execution_flows ex " +
+		//			"INNER JOIN active_executing_flows ax ON ex.exec_id = ax.exec_id";
+		private static String FETCH_ALL_EXECUTABLE_FLOW_HISTORY = 
+				"SELECT exec_id, enc_type, flow_data FROM execution_flows " +
+						"ORDER BY exec_id DESC LIMIT ?, ?";
+		private static String FETCH_EXECUTABLE_FLOW_HISTORY = 
+				"SELECT exec_id, enc_type, flow_data FROM execution_flows " +
+						"WHERE project_id=? AND flow_id=? " +
+						"ORDER BY exec_id DESC LIMIT ?, ?";
+		private static String FETCH_EXECUTABLE_FLOW_BY_STATUS = 
+				"SELECT exec_id, enc_type, flow_data FROM execution_flows " +
+						"WHERE project_id=? AND flow_id=? AND status=? " +
+						"ORDER BY exec_id DESC LIMIT ?, ?";
 		
 		@Override
 		public List<ExecutableFlow> handle(ResultSet rs) throws SQLException {
@@ -863,7 +912,8 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 					EncodingType encType = EncodingType.fromInteger(encodingType);
 					Object flowObj;
 					try {
-						// Convoluted way to inflate strings. Should find common package or helper function.
+						// Convoluted way to inflate strings. Should find common package 
+						// or helper function.
 						if (encType == EncodingType.GZIP) {
 							// Decompress the sucker.
 							String jsonString = GZIPUtils.unGzipString(data, "UTF-8");
@@ -874,7 +924,8 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 							flowObj = JSONUtils.parseJSONFromString(jsonString);
 						}
 						
-						ExecutableFlow exFlow = ExecutableFlow.createExecutableFlowFromObject(flowObj);
+						ExecutableFlow exFlow = 
+								ExecutableFlow.createExecutableFlowFromObject(flowObj);
 						execFlows.add(exFlow);
 					} catch (IOException e) {
 						throw new SQLException("Error retrieving flow data " + id, e);
diff --git a/src/java/azkaban/jobExecutor/AbstractProcessJob.java b/src/java/azkaban/jobExecutor/AbstractProcessJob.java
index df9e1d1..d106ea0 100644
--- a/src/java/azkaban/jobExecutor/AbstractProcessJob.java
+++ b/src/java/azkaban/jobExecutor/AbstractProcessJob.java
@@ -162,7 +162,8 @@ public abstract class AbstractProcessJob extends AbstractJob {
 		File directory = new File(workingDir);
 		File tempFile = null;
 		try {
-			tempFile = File.createTempFile(getId() + "_", "_tmp", directory);
+			// The temp file prefix must be at least 3 characters.
+			tempFile = File.createTempFile(getId() + "_props_", "_tmp", directory);
 			jobProps.storeFlattened(tempFile);
 		} catch (IOException e) {
 			throw new RuntimeException("Failed to create temp property file ", e);
diff --git a/src/java/azkaban/jobExecutor/utils/process/AzkabanProcess.java b/src/java/azkaban/jobExecutor/utils/process/AzkabanProcess.java
index 54179fd..2e5de64 100644
--- a/src/java/azkaban/jobExecutor/utils/process/AzkabanProcess.java
+++ b/src/java/azkaban/jobExecutor/utils/process/AzkabanProcess.java
@@ -162,6 +162,13 @@ public class AzkabanProcess {
 	public void hardKill() {
 		checkStarted();
 		if (isRunning()) {
+			if (processId != 0 ) {
+				try {
+					Runtime.getRuntime().exec("kill -9 " + processId);
+				} catch (IOException e) {
+					logger.error("Kill attempt failed.", e);
+				}
+			}
 			process.destroy();
 		}
 	}
diff --git a/src/java/azkaban/migration/schedule2trigger/CommonParams.java b/src/java/azkaban/migration/schedule2trigger/CommonParams.java
new file mode 100644
index 0000000..0408f51
--- /dev/null
+++ b/src/java/azkaban/migration/schedule2trigger/CommonParams.java
@@ -0,0 +1,22 @@
+package azkaban.migration.schedule2trigger;
+
+public class CommonParams {
+	public static final String TYPE_FLOW_FINISH = "FlowFinish";
+	public static final String TYPE_FLOW_SUCCEED = "FlowSucceed";
+	public static final String TYPE_FLOW_PROGRESS = "FlowProgress";
+
+	public static final String TYPE_JOB_FINISH = "JobFinish";
+	public static final String TYPE_JOB_SUCCEED = "JobSucceed";
+	public static final String TYPE_JOB_PROGRESS = "JobProgress";
+
+	public static final String INFO_DURATION = "Duration";
+	public static final String INFO_FLOW_NAME = "FlowName";
+	public static final String INFO_JOB_NAME = "JobName";
+	public static final String INFO_PROGRESS_PERCENT = "ProgressPercent";
+	public static final String INFO_EMAIL_LIST = "EmailList";
+
+	// always alert
+	public static final String ALERT_TYPE = "SlaAlertType";
+	public static final String ACTION_CANCEL_FLOW = "SlaCancelFlow";
+	public static final String ACTION_ALERT = "SlaAlert";
+}
diff --git a/src/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java b/src/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
new file mode 100644
index 0000000..6d1bd39
--- /dev/null
+++ b/src/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
@@ -0,0 +1,256 @@
+package azkaban.migration.schedule2trigger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+import azkaban.executor.ExecutionOptions;
+import static azkaban.migration.schedule2trigger.CommonParams.*;
+import azkaban.utils.JSONUtils;
+import azkaban.utils.Props;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.ReadablePeriod;
+import azkaban.trigger.Condition;
+import azkaban.trigger.ConditionChecker;
+import azkaban.trigger.JdbcTriggerLoader;
+import azkaban.trigger.Trigger;
+import azkaban.trigger.TriggerAction;
+import azkaban.trigger.TriggerLoader;
+import azkaban.trigger.builtin.BasicTimeChecker;
+import azkaban.trigger.builtin.ExecuteFlowAction;
+import azkaban.utils.Utils;
+
+public class Schedule2Trigger {
+	
+	private static final Logger logger = Logger.getLogger(Schedule2Trigger.class);
+	private static Props props;
+	private static File outputDir;
+	
+	public static void main(String[] args) throws Exception{
+		if(args.length < 1) {
+			printUsage();
+		}
+		
+		File confFile = new File(args[0]);
+		try {
+			logger.info("Trying to load config from " + confFile.getAbsolutePath());
+			props = loadAzkabanConfig(confFile);
+		} catch (Exception e) {
+			e.printStackTrace();
+			logger.error(e);
+			return;
+		}
+		
+		try {
+			outputDir = File.createTempFile("schedules", null);
+			logger.info("Creating temp dir for dumping existing schedules.");
+			outputDir.delete();
+			outputDir.mkdir();
+		} catch (Exception e) {
+			e.printStackTrace();
+			logger.error(e);
+			return;
+		}
+
+		try {
+			schedule2File();
+		} catch (Exception e) {
+			e.printStackTrace();
+			logger.error(e);
+			return;
+		}
+		
+		try {
+			file2ScheduleTrigger();
+		} catch (Exception e) {
+			e.printStackTrace();
+			logger.error(e);
+			return;
+		}
+		
+		logger.info("Uploaded all schedules. Removing temp dir.");
+		FileUtils.deleteDirectory(outputDir);
+		System.exit(0);
+	}
+	
+	private static Props loadAzkabanConfig(File confFile) throws IOException {
+		return new Props(null, confFile);
+	}
+	
+	private static void printUsage() {
+		System.out.println("Usage: schedule2Trigger PATH_TO_CONFIG_FILE");
+	}
+	
+	private static void schedule2File() throws Exception {
+		azkaban.migration.scheduler.ScheduleLoader scheduleLoader = new azkaban.migration.scheduler.JdbcScheduleLoader(props);
+		logger.info("Loading old schedule info from DB.");
+		List<azkaban.migration.scheduler.Schedule> schedules = scheduleLoader.loadSchedules();
+		for(azkaban.migration.scheduler.Schedule sched : schedules) {
+			writeScheduleFile(sched, outputDir);
+		}
+	}
+	
+	private static void writeScheduleFile(azkaban.migration.scheduler.Schedule sched, File outputDir) throws IOException {
+		String scheduleFileName = sched.getProjectName()+"-"+sched.getFlowName();
+		File outputFile = new File(outputDir, scheduleFileName);
+		outputFile.createNewFile();
+		Props props = new Props();
+		props.put("flowName", sched.getFlowName());
+		props.put("projectName", sched.getProjectName());
+		props.put("projectId", String.valueOf(sched.getProjectId()));
+		props.put("period", azkaban.migration.scheduler.Schedule.createPeriodString(sched.getPeriod()));
+		props.put("firstScheduleTimeLong", sched.getFirstSchedTime());
+		props.put("timezone", sched.getTimezone().getID());
+		props.put("submitUser", sched.getSubmitUser());
+		props.put("submitTimeLong", sched.getSubmitTime());
+		props.put("nextExecTimeLong", sched.getNextExecTime());
+		
+		ExecutionOptions executionOptions = sched.getExecutionOptions();
+		if(executionOptions != null) {
+			props.put("executionOptionsObj", JSONUtils.toJSON(executionOptions.toObject()));
+		}
+		
+		azkaban.migration.sla.SlaOptions slaOptions = sched.getSlaOptions();
+		if(slaOptions != null) {
+			
+			List<Map<String, Object>> settingsObj = new ArrayList<Map<String,Object>>();
+			List<azkaban.migration.sla.SLA.SlaSetting> settings = slaOptions.getSettings();
+			for(azkaban.migration.sla.SLA.SlaSetting set : settings) {
+				Map<String, Object> setObj = new HashMap<String, Object>();
+				String setId = set.getId();
+				azkaban.migration.sla.SLA.SlaRule rule = set.getRule();
+				Map<String, Object> info = new HashMap<String, Object>();
+				info.put(INFO_DURATION, azkaban.migration.scheduler.Schedule.createPeriodString(set.getDuration()));
+				info.put(INFO_EMAIL_LIST, slaOptions.getSlaEmails());
+				List<String> actionsList = new ArrayList<String>();
+				for(azkaban.migration.sla.SLA.SlaAction act : set.getActions()) {
+					if(act.equals(azkaban.migration.sla.SLA.SlaAction.EMAIL)) {
+						actionsList.add(ACTION_ALERT);
+						info.put(ALERT_TYPE, "email");
+					} else if(act.equals(azkaban.migration.sla.SLA.SlaAction.KILL)) {
+						actionsList.add(ACTION_CANCEL_FLOW);
+					}
+				}
+				setObj.put("actions", actionsList);
+				if(setId.equals("")) {
+					info.put(INFO_FLOW_NAME, sched.getFlowName());
+					if(rule.equals(azkaban.migration.sla.SLA.SlaRule.FINISH)) {
+						setObj.put("type", TYPE_FLOW_FINISH);
+					} else if(rule.equals(azkaban.migration.sla.SLA.SlaRule.SUCCESS)) {
+						setObj.put("type", TYPE_FLOW_SUCCEED);
+					}
+				} else {
+					info.put(INFO_JOB_NAME, setId);
+					if(rule.equals(azkaban.migration.sla.SLA.SlaRule.FINISH)) {
+						setObj.put("type", TYPE_JOB_FINISH);
+					} else if(rule.equals(azkaban.migration.sla.SLA.SlaRule.SUCCESS)) {
+						setObj.put("type", TYPE_JOB_SUCCEED);
+					}
+				}
+				setObj.put("info", info);
+				settingsObj.add(setObj);
+			}
+			
+			props.put("slaOptionsObj", JSONUtils.toJSON(settingsObj));
+		}
+		props.storeLocal(outputFile);
+	}
+
+	private static void file2ScheduleTrigger() throws Exception {
+		
+		TriggerLoader triggerLoader = new JdbcTriggerLoader(props);
+		for(File scheduleFile : outputDir.listFiles()) {
+			logger.info("Trying to load schedule from " + scheduleFile.getAbsolutePath());
+			if(scheduleFile.isFile()) {
+				Props schedProps = new Props(null, scheduleFile);
+				String flowName = schedProps.getString("flowName");
+				String projectName = schedProps.getString("projectName");
+				int projectId = schedProps.getInt("projectId");
+				long firstSchedTimeLong = schedProps.getLong("firstScheduleTimeLong");
+//				DateTime firstSchedTime = new DateTime(firstSchedTimeLong);
+				String timezoneId = schedProps.getString("timezone");
+				DateTimeZone timezone = DateTimeZone.forID(timezoneId);
+				ReadablePeriod period = Utils.parsePeriodString(schedProps.getString("period"));
+//				DateTime lastModifyTime = DateTime.now();
+				long nextExecTimeLong = schedProps.getLong("nextExecTimeLong");
+//				DateTime nextExecTime = new DateTime(nextExecTimeLong);
+				long submitTimeLong = schedProps.getLong("submitTimeLong");
+//				DateTime submitTime = new DateTime(submitTimeLong);
+				String submitUser = schedProps.getString("submitUser");
+				ExecutionOptions executionOptions = null;
+				if(schedProps.containsKey("executionOptionsObj")) {
+					String executionOptionsObj = schedProps.getString("executionOptionsObj");
+					executionOptions = ExecutionOptions.createFromObject(JSONUtils.parseJSONFromString(executionOptionsObj));
+				} else {
+					executionOptions = new ExecutionOptions();
+				}
+				List<azkaban.sla.SlaOption> slaOptions = null;
+				if(schedProps.containsKey("slaOptionsObj")) {
+					slaOptions = new ArrayList<azkaban.sla.SlaOption>();
+					List<Map<String, Object>> settingsObj = (List<Map<String, Object>>) JSONUtils.parseJSONFromString(schedProps.getString("slaOptionsObj"));
+					for(Map<String, Object> sla : settingsObj) {
+						String type = (String) sla.get("type");
+						Map<String, Object> info = (Map<String, Object>) sla.get("info");
+						List<String> actions = (List<String>) sla.get("actions");
+						azkaban.sla.SlaOption slaOption = new azkaban.sla.SlaOption(type, actions, info);
+						slaOptions.add(slaOption);
+					}
+				}
+				
+				azkaban.scheduler.Schedule schedule = new azkaban.scheduler.Schedule(-1, projectId, projectName, flowName, "ready", firstSchedTimeLong, timezone, period, DateTime.now().getMillis(), nextExecTimeLong, submitTimeLong, submitUser, executionOptions, slaOptions);
+				Trigger t = scheduleToTrigger(schedule);
+				logger.info("Ready to insert trigger " + t.getDescription());
+				triggerLoader.addTrigger(t);
+				
+			}
+			
+		}
+	}
+	
+	
+	private static Trigger scheduleToTrigger(azkaban.scheduler.Schedule s) {
+		
+		Condition triggerCondition = createTimeTriggerCondition(s);
+		Condition expireCondition = createTimeExpireCondition(s);
+		List<TriggerAction> actions = createActions(s);
+		Trigger t = new Trigger(s.getScheduleId(), s.getLastModifyTime(), s.getSubmitTime(), s.getSubmitUser(), azkaban.scheduler.ScheduleManager.triggerSource, triggerCondition, expireCondition, actions);
+		if(s.isRecurring()) {
+			t.setResetOnTrigger(true);
+		}
+		return t;
+	}
+	
+	private static List<TriggerAction> createActions (azkaban.scheduler.Schedule s) {
+		List<TriggerAction> actions = new ArrayList<TriggerAction>();
+		ExecuteFlowAction executeAct = new ExecuteFlowAction("executeFlowAction", s.getProjectId(), s.getProjectName(), s.getFlowName(), s.getSubmitUser(), s.getExecutionOptions(), s.getSlaOptions());
+		actions.add(executeAct);
+		
+		return actions;
+	}
+	
+	private static Condition createTimeTriggerCondition (azkaban.scheduler.Schedule s) {
+		Map<String, ConditionChecker> checkers = new HashMap<String, ConditionChecker>();
+		ConditionChecker checker = new BasicTimeChecker("BasicTimeChecker_1", s.getFirstSchedTime(), s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(), s.getPeriod());
+		checkers.put(checker.getId(), checker);
+		String expr = checker.getId() + ".eval()";
+		Condition cond = new Condition(checkers, expr);
+		return cond;
+	}
+	
+	// if failed to trigger, auto expire?
+	private static Condition createTimeExpireCondition (azkaban.scheduler.Schedule s) {
+		Map<String, ConditionChecker> checkers = new HashMap<String, ConditionChecker>();
+		ConditionChecker checker = new BasicTimeChecker("BasicTimeChecker_2", s.getFirstSchedTime(), s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(), s.getPeriod());
+		checkers.put(checker.getId(), checker);
+		String expr = checker.getId() + ".eval()";
+		Condition cond = new Condition(checkers, expr);
+		return cond;
+	}
+
+}
diff --git a/src/java/azkaban/migration/scheduler/JdbcScheduleLoader.java b/src/java/azkaban/migration/scheduler/JdbcScheduleLoader.java
new file mode 100644
index 0000000..bcef168
--- /dev/null
+++ b/src/java/azkaban/migration/scheduler/JdbcScheduleLoader.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2012 LinkedIn, Inc
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.migration.scheduler;
+
+
+import azkaban.database.DataSourceUtils;
+import azkaban.utils.GZIPUtils;
+import azkaban.utils.JSONUtils;
+import azkaban.utils.Props;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import javax.sql.DataSource;
+import org.apache.commons.dbutils.DbUtils;
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.dbutils.ResultSetHandler;
+import org.apache.log4j.Logger;
+import org.joda.time.DateTimeZone;
+import org.joda.time.ReadablePeriod;
+
+@Deprecated
+public class JdbcScheduleLoader implements ScheduleLoader {
+
+	private static Logger logger = Logger.getLogger(JdbcScheduleLoader.class);
+	
+	public static enum EncodingType {
+		PLAIN(1), GZIP(2);
+
+		private int numVal;
+
+		EncodingType(int numVal) {
+			this.numVal = numVal;
+		}
+
+		public int getNumVal() {
+			return numVal;
+		}
+
+		public static EncodingType fromInteger(int x) {
+			switch (x) {
+			case 1:
+				return PLAIN;
+			case 2:
+				return GZIP;
+			default:
+				return PLAIN;
+			}
+		}
+	}
+	
+	private DataSource dataSource;
+	private EncodingType defaultEncodingType = EncodingType.GZIP;
+	
+	private static final String scheduleTableName = "schedules";
+
+	private static String SELECT_ALL_SCHEDULES =
+			"SELECT project_id, project_name, flow_name, status, first_sched_time, timezone, period, last_modify_time, next_exec_time, submit_time, submit_user, enc_type, schedule_options FROM " + scheduleTableName;
+	
+	private static String INSERT_SCHEDULE = 
+			"INSERT INTO " + scheduleTableName + " ( project_id, project_name, flow_name, status, first_sched_time, timezone, period, last_modify_time, next_exec_time, submit_time, submit_user, enc_type, schedule_options) values (?,?,?,?,?,?,?,?,?,?,?,?,?)";
+	
+	private static String REMOVE_SCHEDULE_BY_KEY = 
+			"DELETE FROM " + scheduleTableName + " WHERE project_id=? AND flow_name=?";
+	
+	private static String UPDATE_SCHEDULE_BY_KEY = 
+			"UPDATE " + scheduleTableName + " SET status=?, first_sched_time=?, timezone=?, period=?, last_modify_time=?, next_exec_time=?, submit_time=?, submit_user=?, enc_type=?, schedule_options=? WHERE project_id=? AND flow_name=?";
+	
+	private static String UPDATE_NEXT_EXEC_TIME = 
+			"UPDATE " + scheduleTableName + " SET next_exec_time=? WHERE project_id=? AND flow_name=?";
+
+	private Connection getConnection() throws ScheduleManagerException {
+		Connection connection = null;
+		try {
+			connection = dataSource.getConnection();
+		} catch (Exception e) {
+			DbUtils.closeQuietly(connection);
+			throw new ScheduleManagerException("Error getting DB connection.", e);
+		}
+		
+		return connection;
+	}
+	
+	public EncodingType getDefaultEncodingType() {
+		return defaultEncodingType;
+	}
+
+	public void setDefaultEncodingType(EncodingType defaultEncodingType) {
+		this.defaultEncodingType = defaultEncodingType;
+	}
+	
+	public JdbcScheduleLoader(Props props) {
+		String databaseType = props.getString("database.type");
+		
+		if (databaseType.equals("mysql")) {
+			int port = props.getInt("mysql.port");
+			String host = props.getString("mysql.host");
+			String database = props.getString("mysql.database");
+			String user = props.getString("mysql.user");
+			String password = props.getString("mysql.password");
+			int numConnections = props.getInt("mysql.numconnections");
+			
+			dataSource = DataSourceUtils.getMySQLDataSource(host, port, database, user, password, numConnections);
+		}
+	}
+
+	@Override
+	public List<Schedule> loadSchedules() throws ScheduleManagerException {
+		logger.info("Loading all schedules from db.");
+		Connection connection = getConnection();
+
+		QueryRunner runner = new QueryRunner();
+		ResultSetHandler<List<Schedule>> handler = new ScheduleResultHandler();
+	
+		List<Schedule> schedules;
+		
+		try {
+			schedules = runner.query(connection, SELECT_ALL_SCHEDULES, handler);
+		} catch (SQLException e) {
+			logger.error(SELECT_ALL_SCHEDULES + " failed.");
+
+			DbUtils.closeQuietly(connection);
+			throw new ScheduleManagerException("Loading schedules from db failed. ", e);
+		} finally {
+			DbUtils.closeQuietly(connection);
+		}
+		
+		logger.info("Now trying to update the schedules");
+		
+		// filter the schedules
+        Iterator<Schedule> scheduleIterator = schedules.iterator();
+        while (scheduleIterator.hasNext()) {
+            Schedule sched = scheduleIterator.next();
+			if(!sched.updateTime()) {
+				logger.info("Schedule " + sched.getScheduleName() + " was scheduled before azkaban start, skipping it.");
+				scheduleIterator.remove();
+				removeSchedule(sched);
+			}
+			else {
+				logger.info("Recurring schedule, need to update next exec time");
+				try {
+					updateNextExecTime(sched);
+				} catch (Exception e) {
+					e.printStackTrace();
+					throw new ScheduleManagerException("Update next execution time failed.", e);
+				} 
+				logger.info("Schedule " + sched.getScheduleName() + " loaded and updated.");
+			}
+		}
+		
+		
+				
+		logger.info("Loaded " + schedules.size() + " schedules.");
+		
+		return schedules;
+	}
+
+	@Override
+	public void removeSchedule(Schedule s) throws ScheduleManagerException {		
+		logger.info("Removing schedule " + s.getScheduleName() + " from db.");
+
+		QueryRunner runner = new QueryRunner(dataSource);
+	
+		try {
+			int removes =  runner.update(REMOVE_SCHEDULE_BY_KEY, s.getProjectId(), s.getFlowName());
+			if (removes == 0) {
+				throw new ScheduleManagerException("No schedule has been removed.");
+			}
+		} catch (SQLException e) {
+			logger.error(REMOVE_SCHEDULE_BY_KEY + " failed.");
+			throw new ScheduleManagerException("Remove schedule " + s.getScheduleName() + " from db failed. ", e);
+		}
+	}
+	
+	
+	public void insertSchedule(Schedule s) throws ScheduleManagerException {
+		logger.info("Inserting schedule " + s.getScheduleName() + " into db.");
+		insertSchedule(s, defaultEncodingType);
+	}
+
+	public void insertSchedule(Schedule s, EncodingType encType) throws ScheduleManagerException {
+		
+		String json = JSONUtils.toJSON(s.optionsToObject());
+		byte[] data = null;
+		try {
+			byte[] stringData = json.getBytes("UTF-8");
+			data = stringData;
+	
+			if (encType == EncodingType.GZIP) {
+				data = GZIPUtils.gzipBytes(stringData);
+			}
+			logger.debug("NumChars: " + json.length() + " UTF-8:" + stringData.length + " Gzip:"+ data.length);
+		}
+		catch (IOException e) {
+			throw new ScheduleManagerException("Error encoding the schedule options. " + s.getScheduleName());
+		}
+		
+		QueryRunner runner = new QueryRunner(dataSource);
+		try {
+			int inserts =  runner.update( 
+					INSERT_SCHEDULE, 
+					s.getProjectId(),
+					s.getProjectName(),
+					s.getFlowName(), 
+					s.getStatus(), 
+					s.getFirstSchedTime(), 
+					s.getTimezone().getID(), 
+					Schedule.createPeriodString(s.getPeriod()), 
+					s.getLastModifyTime(), 
+					s.getNextExecTime(), 
+					s.getSubmitTime(), 
+					s.getSubmitUser(),
+					encType.getNumVal(),
+					data);
+			if (inserts == 0) {
+				throw new ScheduleManagerException("No schedule has been inserted.");
+			}
+		} catch (SQLException e) {
+			logger.error(INSERT_SCHEDULE + " failed.");
+			throw new ScheduleManagerException("Insert schedule " + s.getScheduleName() + " into db failed. ", e);
+		}
+	}
+	
+	@Override
+	public void updateNextExecTime(Schedule s) throws ScheduleManagerException 
+	{
+		logger.info("Update schedule " + s.getScheduleName() + " into db. ");
+		Connection connection = getConnection();
+		QueryRunner runner = new QueryRunner();
+		try {
+			
+			runner.update(connection, UPDATE_NEXT_EXEC_TIME, s.getNextExecTime(), s.getProjectId(), s.getFlowName()); 
+		} catch (SQLException e) {
+			e.printStackTrace();
+			logger.error(UPDATE_NEXT_EXEC_TIME + " failed.", e);
+			throw new ScheduleManagerException("Update schedule " + s.getScheduleName() + " into db failed. ", e);
+		} finally {
+			DbUtils.closeQuietly(connection);
+		}
+	}
+	
+	@Override
+	public void updateSchedule(Schedule s) throws ScheduleManagerException {
+		logger.info("Updating schedule " + s.getScheduleName() + " into db.");
+		updateSchedule(s, defaultEncodingType);
+	}
+		
+	public void updateSchedule(Schedule s, EncodingType encType) throws ScheduleManagerException {
+
+		String json = JSONUtils.toJSON(s.optionsToObject());
+		byte[] data = null;
+		try {
+			byte[] stringData = json.getBytes("UTF-8");
+			data = stringData;
+	
+			if (encType == EncodingType.GZIP) {
+				data = GZIPUtils.gzipBytes(stringData);
+			}
+			logger.debug("NumChars: " + json.length() + " UTF-8:" + stringData.length + " Gzip:"+ data.length);
+		}
+		catch (IOException e) {
+			throw new ScheduleManagerException("Error encoding the schedule options " + s.getScheduleName());
+		}
+
+		QueryRunner runner = new QueryRunner(dataSource);
+	
+		try {
+			int updates =  runner.update( 
+					UPDATE_SCHEDULE_BY_KEY, 
+					s.getStatus(), 
+					s.getFirstSchedTime(), 
+					s.getTimezone().getID(), 
+					Schedule.createPeriodString(s.getPeriod()), 
+					s.getLastModifyTime(), 
+					s.getNextExecTime(), 
+					s.getSubmitTime(), 
+					s.getSubmitUser(), 	
+					encType.getNumVal(),
+					data,
+					s.getProjectId(), 
+					s.getFlowName());
+			if (updates == 0) {
+				throw new ScheduleManagerException("No schedule has been updated.");
+			}
+		} catch (SQLException e) {
+			logger.error(UPDATE_SCHEDULE_BY_KEY + " failed.");
+			throw new ScheduleManagerException("Update schedule " + s.getScheduleName() + " into db failed. ", e);
+		}
+	}
+
+	public class ScheduleResultHandler implements ResultSetHandler<List<Schedule>> {
+		@Override
+		public List<Schedule> handle(ResultSet rs) throws SQLException {
+			if (!rs.next()) {
+				return Collections.<Schedule>emptyList();
+			}
+			
+			ArrayList<Schedule> schedules = new ArrayList<Schedule>();
+			do {
+				int projectId = rs.getInt(1);
+				String projectName = rs.getString(2);
+				String flowName = rs.getString(3);
+				String status = rs.getString(4);
+				long firstSchedTime = rs.getLong(5);
+				DateTimeZone timezone = DateTimeZone.forID(rs.getString(6));
+				ReadablePeriod period = Schedule.parsePeriodString(rs.getString(7));
+				long lastModifyTime = rs.getLong(8);
+				long nextExecTime = rs.getLong(9);
+				long submitTime = rs.getLong(10);
+				String submitUser = rs.getString(11);
+				int encodingType = rs.getInt(12);
+				byte[] data = rs.getBytes(13);
+				
+				Object optsObj = null;
+				if (data != null) {
+					EncodingType encType = EncodingType.fromInteger(encodingType);
+
+					try {
+						// Convoluted way to inflate strings. Should find common package or helper function.
+						if (encType == EncodingType.GZIP) {
+							// Decompress the sucker.
+							String jsonString = GZIPUtils.unGzipString(data, "UTF-8");
+							optsObj = JSONUtils.parseJSONFromString(jsonString);
+						}
+						else {
+							String jsonString = new String(data, "UTF-8");
+							optsObj = JSONUtils.parseJSONFromString(jsonString);
+						}	
+					} catch (IOException e) {
+						throw new SQLException("Error reconstructing schedule options " + projectName + "." + flowName);
+					}
+				}
+				
+				Schedule s = new Schedule(projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser);
+				if (optsObj != null) {
+					s.createAndSetScheduleOptions(optsObj);
+				}
+				
+				schedules.add(s);
+			} while (rs.next());
+			
+			return schedules;
+		}
+		
+	}
+}
\ No newline at end of file
diff --git a/src/java/azkaban/migration/scheduler/Schedule.java b/src/java/azkaban/migration/scheduler/Schedule.java
new file mode 100644
index 0000000..9490243
--- /dev/null
+++ b/src/java/azkaban/migration/scheduler/Schedule.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2012 LinkedIn, Inc
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.migration.scheduler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Days;
+import org.joda.time.DurationFieldType;
+import org.joda.time.Hours;
+import org.joda.time.Minutes;
+import org.joda.time.Months;
+import org.joda.time.ReadablePeriod;
+import org.joda.time.Seconds;
+import org.joda.time.Weeks;
+
+import azkaban.executor.ExecutionOptions;
+import azkaban.migration.sla.SlaOptions;
+import azkaban.utils.Pair;
+
+@Deprecated
+public class Schedule{
+	
+//	private long projectGuid;
+//	private long flowGuid;
+	
+//	private String scheduleId;
+	
+	private int projectId;
+	private String projectName;
+	private String flowName;
+	private long firstSchedTime;
+	private DateTimeZone timezone;
+	private long lastModifyTime;
+	private ReadablePeriod period;
+	private long nextExecTime;
+	private String submitUser;
+	private String status;
+	private long submitTime;
+	
+	private ExecutionOptions executionOptions;
+	private SlaOptions slaOptions;
+	
+	public Schedule(
+						int projectId,
+						String projectName,
+						String flowName,
+						String status,
+						long firstSchedTime,
+						DateTimeZone timezone,
+						ReadablePeriod period,
+						long lastModifyTime,						
+						long nextExecTime,						
+						long submitTime,
+						String submitUser
+						) {
+		this.projectId = projectId;
+		this.projectName = projectName;
+		this.flowName = flowName;
+		this.firstSchedTime = firstSchedTime;
+		this.timezone = timezone;
+		this.lastModifyTime = lastModifyTime;
+		this.period = period;
+		this.nextExecTime = nextExecTime;
+		this.submitUser = submitUser;
+		this.status = status;
+		this.submitTime = submitTime;
+		this.executionOptions = null;
+		this.slaOptions = null;
+	}
+
+	public Schedule(
+						int projectId,
+						String projectName,
+						String flowName,
+						String status,
+						long firstSchedTime,
+						String timezoneId,
+						String period,
+						long lastModifyTime,						
+						long nextExecTime,						
+						long submitTime,
+						String submitUser,
+						ExecutionOptions executionOptions,
+						SlaOptions slaOptions
+			) {
+		this.projectId = projectId;
+		this.projectName = projectName;
+		this.flowName = flowName;
+		this.firstSchedTime = firstSchedTime;
+		this.timezone = DateTimeZone.forID(timezoneId);
+		this.lastModifyTime = lastModifyTime;
+		this.period = parsePeriodString(period);
+		this.nextExecTime = nextExecTime;
+		this.submitUser = submitUser;
+		this.status = status;
+		this.submitTime = submitTime;
+		this.executionOptions = executionOptions;
+		this.slaOptions = slaOptions;
+	}
+
+	public Schedule(
+						int projectId,
+						String projectName,
+						String flowName,
+						String status,
+						long firstSchedTime,
+						DateTimeZone timezone,
+						ReadablePeriod period,
+						long lastModifyTime,
+						long nextExecTime,
+						long submitTime,
+						String submitUser,
+						ExecutionOptions executionOptions,
+						SlaOptions slaOptions
+						) {
+		this.projectId = projectId;
+		this.projectName = projectName;
+		this.flowName = flowName;
+		this.firstSchedTime = firstSchedTime;
+		this.timezone = timezone;
+		this.lastModifyTime = lastModifyTime;
+		this.period = period;
+		this.nextExecTime = nextExecTime;
+		this.submitUser = submitUser;
+		this.status = status;
+		this.submitTime = submitTime;
+		this.executionOptions = executionOptions;
+		this.slaOptions = slaOptions;
+	}
+
+	public ExecutionOptions getExecutionOptions() {
+		return executionOptions;
+	}
+
+	public void setFlowOptions(ExecutionOptions executionOptions) {
+		this.executionOptions = executionOptions;
+	}
+
+	public SlaOptions getSlaOptions() {
+		return slaOptions;
+	}
+
+	public void setSlaOptions(SlaOptions slaOptions) {
+		this.slaOptions = slaOptions;
+	}
+
+	public String getScheduleName() {
+		return projectName + "." + flowName + " (" + projectId + ")";
+	}
+	
+	public String toString() {
+		return projectName + "." + flowName + " (" + projectId + ")" + " to be run at (starting) " + 
+				new DateTime(firstSchedTime).toDateTimeISO() + " with recurring period of " + (period == null ? "non-recurring" : createPeriodString(period));
+	}
+	
+	public Pair<Integer, String> getScheduleId() {
+		return new Pair<Integer, String>(getProjectId(), getFlowName());
+	}
+	
+	public int getProjectId() {
+		return projectId;
+	}
+
+	public String getProjectName() {
+		return projectName;
+	}
+
+	public String getFlowName() {
+		return flowName;
+	}
+
+	public long getFirstSchedTime() {
+		return firstSchedTime;
+	}
+
+	public DateTimeZone getTimezone() {
+		return timezone;
+	}
+
+	public long getLastModifyTime() {
+		return lastModifyTime;
+	}
+
+	public ReadablePeriod getPeriod() {
+		return period;
+	}
+
+	public long getNextExecTime() {
+		return nextExecTime;
+	}
+
+	public String getSubmitUser() {
+		return submitUser;
+	}
+
+	public String getStatus() {
+		return status;
+	}
+
+	public long getSubmitTime() {
+		return submitTime;
+	}
+
+	public boolean updateTime() {
+		if (new DateTime(nextExecTime).isAfterNow()) {
+			return true;
+		}
+
+		if (period != null) {
+			DateTime nextTime = getNextRuntime(nextExecTime, timezone, period);
+
+			this.nextExecTime = nextTime.getMillis();
+			return true;
+		}
+
+		return false;
+	}
+	
+	private DateTime getNextRuntime(long scheduleTime, DateTimeZone timezone, ReadablePeriod period) {
+		DateTime now = new DateTime();
+		DateTime date = new DateTime(scheduleTime).withZone(timezone);
+		int count = 0;
+		while (!now.isBefore(date)) {
+			if (count > 100000) {
+				throw new IllegalStateException(
+						"100000 increments of period did not get to present time.");
+			}
+
+			if (period == null) {
+				break;
+			} else {
+				date = date.plus(period);
+			}
+
+			count += 1;
+		}
+
+		return date;
+	}
+
+	public static ReadablePeriod parsePeriodString(String periodStr) {
+		ReadablePeriod period;
+		char periodUnit = periodStr.charAt(periodStr.length() - 1);
+		if (periodUnit == 'n') {
+			return null;
+		}
+
+		int periodInt = Integer.parseInt(periodStr.substring(0,
+				periodStr.length() - 1));
+		switch (periodUnit) {
+		case 'M':
+			period = Months.months(periodInt);
+			break;
+		case 'w':
+			period = Weeks.weeks(periodInt);
+			break;
+		case 'd':
+			period = Days.days(periodInt);
+			break;
+		case 'h':
+			period = Hours.hours(periodInt);
+			break;
+		case 'm':
+			period = Minutes.minutes(periodInt);
+			break;
+		case 's':
+			period = Seconds.seconds(periodInt);
+			break;
+		default:
+			throw new IllegalArgumentException("Invalid schedule period unit '"
+					+ periodUnit);
+		}
+
+		return period;
+	}
+
+	public static String createPeriodString(ReadablePeriod period) {
+		String periodStr = "n";
+
+		if (period == null) {
+			return "n";
+		}
+
+		if (period.get(DurationFieldType.months()) > 0) {
+			int months = period.get(DurationFieldType.months());
+			periodStr = months + "M";
+		} else if (period.get(DurationFieldType.weeks()) > 0) {
+			int weeks = period.get(DurationFieldType.weeks());
+			periodStr = weeks + "w";
+		} else if (period.get(DurationFieldType.days()) > 0) {
+			int days = period.get(DurationFieldType.days());
+			periodStr = days + "d";
+		} else if (period.get(DurationFieldType.hours()) > 0) {
+			int hours = period.get(DurationFieldType.hours());
+			periodStr = hours + "h";
+		} else if (period.get(DurationFieldType.minutes()) > 0) {
+			int minutes = period.get(DurationFieldType.minutes());
+			periodStr = minutes + "m";
+		} else if (period.get(DurationFieldType.seconds()) > 0) {
+			int seconds = period.get(DurationFieldType.seconds());
+			periodStr = seconds + "s";
+		}
+
+		return periodStr;
+	}
+	
+	
+	public Map<String,Object> optionsToObject() {
+		if(executionOptions != null || slaOptions != null) {
+			HashMap<String, Object> schedObj = new HashMap<String, Object>();
+			
+			if(executionOptions != null) {
+				schedObj.put("executionOptions", executionOptions.toObject());
+			}
+			if(slaOptions != null) {
+				schedObj.put("slaOptions", slaOptions.toObject());
+			}
+	
+			return schedObj;
+		}
+		return null;
+	}
+	
+	public void createAndSetScheduleOptions(Object obj) {
+		@SuppressWarnings("unchecked")
+		HashMap<String, Object> schedObj = (HashMap<String, Object>)obj;
+		if (schedObj.containsKey("executionOptions")) {
+			ExecutionOptions execOptions = ExecutionOptions.createFromObject(schedObj.get("executionOptions"));
+			this.executionOptions = execOptions;
+		}
+		else if (schedObj.containsKey("flowOptions")){
+			ExecutionOptions execOptions = ExecutionOptions.createFromObject(schedObj.get("flowOptions"));
+			this.executionOptions = execOptions;
+			execOptions.setConcurrentOption(ExecutionOptions.CONCURRENT_OPTION_SKIP);
+		}
+		else {
+			this.executionOptions = new ExecutionOptions();
+			this.executionOptions.setConcurrentOption(ExecutionOptions.CONCURRENT_OPTION_SKIP);
+		}
+
+		if (schedObj.containsKey("slaOptions")) {
+			SlaOptions slaOptions = SlaOptions.fromObject(schedObj.get("slaOptions"));
+			this.slaOptions = slaOptions;
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/java/azkaban/migration/scheduler/ScheduleLoader.java b/src/java/azkaban/migration/scheduler/ScheduleLoader.java
new file mode 100644
index 0000000..6511d9c
--- /dev/null
+++ b/src/java/azkaban/migration/scheduler/ScheduleLoader.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2012 LinkedIn, Inc
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.migration.scheduler;
+
+import java.util.List;
+
+@Deprecated
+public interface ScheduleLoader {
+	
+	public void insertSchedule(Schedule s) throws ScheduleManagerException;
+	
+	public void updateSchedule(Schedule s) throws ScheduleManagerException;
+	
+	public List<Schedule> loadSchedules() throws ScheduleManagerException;
+	
+	public void removeSchedule(Schedule s) throws ScheduleManagerException;
+
+	public void updateNextExecTime(Schedule s) throws ScheduleManagerException;
+
+}
\ No newline at end of file
diff --git a/src/java/azkaban/migration/scheduler/ScheduleManagerException.java b/src/java/azkaban/migration/scheduler/ScheduleManagerException.java
new file mode 100644
index 0000000..f0f6705
--- /dev/null
+++ b/src/java/azkaban/migration/scheduler/ScheduleManagerException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012 LinkedIn, Inc
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.migration.scheduler;
+
+@Deprecated
+public class ScheduleManagerException extends Exception{
+	private static final long serialVersionUID = 1L;
+
+	public ScheduleManagerException(String message) {
+		super(message);
+	}
+	
+	public ScheduleManagerException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
\ No newline at end of file
diff --git a/src/java/azkaban/migration/sla/SLA.java b/src/java/azkaban/migration/sla/SLA.java
new file mode 100644
index 0000000..0e75965
--- /dev/null
+++ b/src/java/azkaban/migration/sla/SLA.java
@@ -0,0 +1,252 @@
+package azkaban.migration.sla;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.ReadablePeriod;
+
+import azkaban.migration.scheduler.Schedule;
+
+@Deprecated
+public class SLA {
+
+	public static enum SlaRule {
+		SUCCESS(1), FINISH(2), WAITANDCHECKJOB(3);
+
+		private int numVal;
+
+		SlaRule(int numVal) {
+			this.numVal = numVal;
+		}
+
+		public int getNumVal() {
+			return numVal;
+		}
+
+		public static SlaRule fromInteger(int x) {
+			switch (x) {
+			case 1:
+				return SUCCESS;
+			case 2:
+				return FINISH;
+			case 3:
+				return WAITANDCHECKJOB;
+			default:
+				return SUCCESS;
+			}
+		}
+	}
+	
+	public static enum SlaAction {
+		EMAIL(1), KILL(2);
+
+		private int numVal;
+
+		SlaAction(int numVal) {
+			this.numVal = numVal;
+		}
+
+		public int getNumVal() {
+			return numVal;
+		}
+
+		public static SlaAction fromInteger(int x) {
+			switch (x) {
+			case 1:
+				return EMAIL;
+			case 2:
+				return KILL;
+			default:
+				return EMAIL;
+			}
+		}
+	}
+	
+	public static class SlaSetting {
+		public String getId() {
+			return id;
+		}
+		public void setId(String id) {
+			this.id = id;
+		}
+		public ReadablePeriod getDuration() {
+			return duration;
+		}
+		public void setDuration(ReadablePeriod duration) {
+			this.duration = duration;
+		}
+		public SlaRule getRule() {
+			return rule;
+		}
+		public void setRule(SlaRule rule) {
+			this.rule = rule;
+		}
+		public List<SlaAction> getActions() {
+			return actions;
+		}
+		public void setActions(List<SlaAction> actions) {
+			this.actions = actions;
+		}
+		
+		public Object toObject() {
+			Map<String, Object> obj = new HashMap<String, Object>();
+			obj.put("id", id);
+			obj.put("duration", Schedule.createPeriodString(duration));
+//			List<String> rulesObj = new ArrayList<String>();
+//			for(SlaRule rule : rules) {
+//				rulesObj.add(rule.toString());
+//			}
+//			obj.put("rules", rulesObj);
+			obj.put("rule", rule.toString());
+			List<String> actionsObj = new ArrayList<String>();
+			for(SlaAction act : actions) {
+				actionsObj.add(act.toString());
+			}
+			obj.put("actions", actionsObj);
+			return obj;
+		}
+		
+		@SuppressWarnings("unchecked")
+		public static SlaSetting fromObject(Object obj) {
+			Map<String, Object> slaObj = (HashMap<String, Object>) obj;
+			String subId = (String) slaObj.get("id");
+			ReadablePeriod dur = Schedule.parsePeriodString((String) slaObj.get("duration"));
+//			List<String> rulesObj = (ArrayList<String>) slaObj.get("rules");
+//			List<SlaRule> slaRules = new ArrayList<SLA.SlaRule>();
+//			for(String rule : rulesObj) {
+//				slaRules.add(SlaRule.valueOf(rule));
+//			}
+			SlaRule slaRule = SlaRule.valueOf((String) slaObj.get("rule"));
+			List<String> actsObj = (ArrayList<String>) slaObj.get("actions");
+			List<SlaAction> slaActs = new ArrayList<SlaAction>();
+			for(String act : actsObj) {
+				slaActs.add(SlaAction.valueOf(act));
+			}
+			
+			SlaSetting ret = new SlaSetting();
+			ret.setId(subId);
+			ret.setDuration(dur);
+			ret.setRule(slaRule);
+			ret.setActions(slaActs);
+			return ret;
+		}
+		
+		private String id;
+		private ReadablePeriod duration;
+		private SlaRule rule = SlaRule.SUCCESS;
+		private List<SlaAction> actions;
+	}
+	
+	private int execId;
+	private String jobName;
+	private DateTime checkTime;
+	private List<String> emails;
+	private List<SlaAction> actions;
+	private List<SlaSetting> jobSettings;
+	private SlaRule rule;
+	
+	public SLA(
+			int execId,
+			String jobName,
+			DateTime checkTime,
+			List<String> emails,
+			List<SlaAction> slaActions,
+			List<SlaSetting> jobSettings,
+			SlaRule slaRule
+	) {
+		this.execId = execId;
+		this.jobName = jobName;
+		this.checkTime = checkTime;
+		this.emails = emails;
+		this.actions = slaActions;
+		this.jobSettings = jobSettings;
+		this.rule = slaRule;
+	}
+
+	public int getExecId() {
+		return execId;
+	}
+
+	public String getJobName() {
+		return jobName;
+	}
+
+	public DateTime getCheckTime() {
+		return checkTime;
+	}
+
+	public List<String> getEmails() {
+		return emails;
+	}
+
+	public List<SlaAction> getActions() {
+		return actions;
+	}
+	
+	public List<SlaSetting> getJobSettings() {
+		return jobSettings;
+	}
+
+	public SlaRule getRule() {
+		return rule;
+	}
+	
+	public String toString() {
+		return execId + " " + jobName + " to be checked at " + checkTime.toDateTimeISO();
+	}
+	
+	public Map<String,Object> optionToObject() {
+		HashMap<String, Object> slaObj = new HashMap<String, Object>();
+
+		slaObj.put("emails", emails);
+//		slaObj.put("rule", rule.toString());
+
+		List<String> actionsObj = new ArrayList<String>();
+		for(SlaAction act : actions) {
+			actionsObj.add(act.toString());
+		}
+		slaObj.put("actions", actionsObj);
+		
+		if(jobSettings != null && jobSettings.size() > 0) {
+			List<Object> settingsObj = new ArrayList<Object>();
+			for(SlaSetting set : jobSettings) {
+				settingsObj.add(set.toObject());
+			}
+			slaObj.put("jobSettings", settingsObj);
+		}
+
+		return slaObj;
+	}
+
+	@SuppressWarnings("unchecked")
+	public static SLA createSlaFromObject(int execId, String jobName, DateTime checkTime, SlaRule rule, Object obj) {
+		
+		HashMap<String, Object> slaObj = (HashMap<String,Object>)obj;
+
+		List<String> emails = (List<String>)slaObj.get("emails");
+//		SlaRule rule = SlaRule.valueOf((String)slaObj.get("rule"));
+		List<String> actsObj = (ArrayList<String>) slaObj.get("actions");
+		List<SlaAction> slaActs = new ArrayList<SlaAction>();
+		for(String act : actsObj) {
+			slaActs.add(SlaAction.valueOf(act));
+		}
+		List<SlaSetting> jobSets = null;
+		if(slaObj.containsKey("jobSettings") && slaObj.get("jobSettings") != null) {
+			jobSets = new ArrayList<SLA.SlaSetting>();
+			for(Object set : (List<Object>)slaObj.get("jobSettings")) {
+				SlaSetting jobSet = SlaSetting.fromObject(set);
+				jobSets.add(jobSet);
+			}
+		}
+		
+		return new SLA(execId, jobName, checkTime, emails, slaActs, jobSets, rule);
+	}
+
+	public void setCheckTime(DateTime time) {
+		this.checkTime = time;
+	}
+
+}
diff --git a/src/java/azkaban/migration/sla/SlaOptions.java b/src/java/azkaban/migration/sla/SlaOptions.java
new file mode 100644
index 0000000..f7b9d49
--- /dev/null
+++ b/src/java/azkaban/migration/sla/SlaOptions.java
@@ -0,0 +1,52 @@
+package azkaban.migration.sla;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import azkaban.migration.sla.SLA.SlaSetting;
+
+@Deprecated
+public class SlaOptions {
+
+	public List<String> getSlaEmails() {
+		return slaEmails;
+	}
+	public void setSlaEmails(List<String> slaEmails) {
+		this.slaEmails = slaEmails;
+	}
+	public List<SlaSetting> getSettings() {
+		return settings;
+	}
+	public void setSettings(List<SlaSetting> settings) {
+		this.settings = settings;
+	}
+	private List<String> slaEmails;
+	private List<SlaSetting> settings;
+	public Object toObject() {
+		Map<String, Object> obj = new HashMap<String, Object>();
+		obj.put("slaEmails", slaEmails);
+		List<Object> slaSettings = new ArrayList<Object>();
+		for(SlaSetting s : settings) {
+			slaSettings.add(s.toObject());
+		}
+		obj.put("settings", slaSettings);
+		return obj;
+	}
+	@SuppressWarnings("unchecked")
+	public static SlaOptions fromObject(Object object) {
+		if(object != null) {
+			SlaOptions slaOptions = new SlaOptions();
+			Map<String, Object> obj = (HashMap<String, Object>) object;
+			slaOptions.setSlaEmails((List<String>) obj.get("slaEmails"));
+			List<SlaSetting> slaSets = new ArrayList<SlaSetting>();
+			for(Object set: (List<Object>)obj.get("settings")) {
+				slaSets.add(SlaSetting.fromObject(set));
+			}
+			slaOptions.setSettings(slaSets);
+			return slaOptions;
+		}
+		return null;			
+	}
+}
\ No newline at end of file
diff --git a/src/java/azkaban/project/ProjectManager.java b/src/java/azkaban/project/ProjectManager.java
index a5be083..be494e4 100644
--- a/src/java/azkaban/project/ProjectManager.java
+++ b/src/java/azkaban/project/ProjectManager.java
@@ -114,6 +114,16 @@ public class ProjectManager {
 		return array;
 	}
 
+  public List<Project> getGroupProjects(User user) {
+    List<Project> array = new ArrayList<Project>();
+    for (Project project : projectsById.values()) {
+      if (project.hasGroupPermission(user, Type.READ)) {
+        array.add(project);
+      }
+    }
+    return array;
+  }
+
 	public List<Project> getUserProjectsByRegex(User user, String regexPattern) {
 		List<Project> array = new ArrayList<Project>();
 		Pattern pattern;
diff --git a/src/java/azkaban/webapp/servlet/ExecutorServlet.java b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
index 6cc091b..55f5d5b 100644
--- a/src/java/azkaban/webapp/servlet/ExecutorServlet.java
+++ b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
@@ -17,6 +17,7 @@
 package azkaban.webapp.servlet;
 
 import java.io.IOException;
+import java.io.File;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -44,7 +45,7 @@ import azkaban.user.Permission;
 import azkaban.user.User;
 import azkaban.user.Permission.Type;
 import azkaban.utils.FileIOUtils.LogData;
-import azkaban.utils.LogSummary;
+import azkaban.utils.JSONUtils;
 import azkaban.webapp.AzkabanWebServer;
 import azkaban.webapp.session.Session;
 
@@ -55,6 +56,8 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 	private ScheduleManager scheduleManager;
 	private ExecutorVelocityHelper velocityHelper;
 
+  private String statsDir;
+
 	@Override
 	public void init(ServletConfig config) throws ServletException {
 		super.init(config);
@@ -63,6 +66,7 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 		executorManager = server.getExecutorManager();
 		scheduleManager = server.getScheduleManager();
 		velocityHelper = new ExecutorVelocityHelper();
+    statsDir = server.getServerProps().getString("azkaban.stats.dir");
 	}
 
 	@Override
@@ -128,9 +132,9 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 				else if (ajaxName.equals("fetchExecJobLogs")) {
 					ajaxFetchJobLogs(req, resp, ret, session.getUser(), exFlow);
 				}
-				else if (ajaxName.equals("fetchExecJobSummary")) {
-					ajaxFetchJobSummary(req, resp, ret, session.getUser(), exFlow);
-				}
+        else if (ajaxName.equals("fetchExecJobStats")) {
+          ajaxFetchJobStats(req, resp, ret, session.getUser(), exFlow);
+        }
 				else if (ajaxName.equals("retryFailedJobs")) {
 					ajaxRestartFailed(req, resp, ret, session.getUser(), exFlow);
 				}
@@ -451,52 +455,41 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 		}
 	}
 	
-	/**
-	 * Gets the job summary.
-	 * 
-	 * @param req
-	 * @param resp
-	 * @param user
-	 * @param exFlow
-	 * @throws ServletException
-	 */
-	private void ajaxFetchJobSummary(HttpServletRequest req, HttpServletResponse resp, HashMap<String, Object> ret, User user, ExecutableFlow exFlow) throws ServletException {
-		Project project = getProjectAjaxByPermission(ret, exFlow.getProjectId(), user, Type.READ);
+  private void ajaxFetchJobStats(
+      HttpServletRequest req, 
+      HttpServletResponse resp, 
+      HashMap<String, Object> ret, 
+      User user, 
+      ExecutableFlow exFlow) throws ServletException {
+		Project project = getProjectAjaxByPermission(
+        ret, exFlow.getProjectId(), user, Type.READ);
 		if (project == null) {
 			return;
 		}
 		
-		String jobId = this.getParam(req, "jobId");
+		String jobId = this.getParam(req, "jobid");
 		resp.setCharacterEncoding("utf-8");
-
+    String statsFilePath = null;
 		try {
 			ExecutableNode node = exFlow.getExecutableNode(jobId);
 			if (node == null) {
-				ret.put("error", "Job " + jobId + " doesn't exist in " + exFlow.getExecutionId());
+				ret.put("error", "Job " + jobId + " doesn't exist in " + 
+            exFlow.getExecutionId());
 				return;
 			}
-			
-			int attempt = this.getIntParam(req, "attempt", node.getAttempt());
-			LogData data = executorManager.getExecutionJobLog(exFlow, jobId, 0, Integer.MAX_VALUE, attempt);
-			
-			LogSummary summary = new LogSummary(data);
-			ret.put("commandProperties", summary.getCommandProperties());
-			
-			String jobType = summary.getJobType();
-			
-			if (jobType.contains("pig")) {
-				ret.put("summaryTableHeaders", summary.getPigSummaryTableHeaders());
-				ret.put("summaryTableData", summary.getPigSummaryTableData());
-				ret.put("statTableHeaders", summary.getPigStatTableHeaders());
-				ret.put("statTableData", summary.getPigStatTableData());
-			} else if (jobType.contains("hive")) {
-				ret.put("hiveQueries", summary.getHiveQueries());
-				ret.put("hiveQueryJobs", summary.getHiveQueryJobs());
-			}
-		} catch (ExecutorManagerException e) {
-			throw new ServletException(e);
-		}
-	}
+	
+      statsFilePath = statsDir + "/" + exFlow.getExecutionId() + "-" + 
+          jobId + "-stats.json";
+      File statsFile = new File(statsFilePath);
+      List<Object> jsonObj = 
+          (ArrayList<Object>) JSONUtils.parseJSONFromFile(statsFile);
+      ret.put("jobStats", jsonObj);
+    }
+    catch (IOException e) {
+      ret.put("error", "Cannot open stats file: " + statsFilePath);
+      return;
+		}
+  }
 
 	private void ajaxFetchFlowInfo(HttpServletRequest req, HttpServletResponse resp, HashMap<String, Object> ret, User user, String projectName, String flowId) throws ServletException {
 		Project project = getProjectAjaxByPermission(ret, projectName, user, Type.READ);
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index 662bad0..4fdcb5c 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -47,6 +47,7 @@ import azkaban.executor.ExecutableFlow;
 import azkaban.executor.ExecutableJobInfo;
 import azkaban.executor.ExecutorManagerAdapter;
 import azkaban.executor.ExecutorManagerException;
+import azkaban.executor.Status;
 import azkaban.flow.Edge;
 import azkaban.flow.Flow;
 import azkaban.flow.FlowProps;
@@ -248,6 +249,11 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 				ajaxFetchFlowExecutions(project, ret, req);
 			}
 		}
+		else if (ajaxName.equals("fetchLastSuccessfulFlowExecution")) {
+			if (handleAjaxPermission(project, user, Type.READ, ret)) {
+				ajaxFetchLastSuccessfulFlowExecution(project, ret, req);
+			}
+		}
 		else if (ajaxName.equals("fetchJobInfo")) {
 			if (handleAjaxPermission(project, user, Type.READ, ret)) {
 				ajaxFetchJobInfo(project, ret, req);
@@ -339,7 +345,34 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 		}
   }
 
-	private void ajaxFetchFlowExecutions(Project project, HashMap<String, Object> ret, HttpServletRequest req) throws ServletException {
+  private void ajaxFetchLastSuccessfulFlowExecution(Project project,
+      HashMap<String, Object> ret, HttpServletRequest req)
+      throws ServletException {
+    String flowId = getParam(req, "flow");
+    List<ExecutableFlow> exFlows = null;
+    try {
+			exFlows = executorManager.getExecutableFlows(
+					project.getId(), flowId, 0, 1, Status.SUCCEEDED);
+		}
+		catch (ExecutorManagerException e) {
+			ret.put("error", "Error retrieving executable flows");
+			return;
+		}
+
+		if (exFlows.size() == 0) {
+			ret.put("success", "false");
+			ret.put("message", "This flow has no successful run.");
+			return;
+		}
+
+		ret.put("success", "true");
+		ret.put("message", "");
+		ret.put("execId", exFlows.get(0).getExecutionId());
+  }
+
+	private void ajaxFetchFlowExecutions(Project project, 
+      HashMap<String, Object> ret, HttpServletRequest req) 
+      throws ServletException {
 		String flowId = getParam(req, "flow");
 		int from = Integer.valueOf(getParam(req, "start"));
 		int length = Integer.valueOf(getParam(req, "length"));
@@ -347,8 +380,10 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 		ArrayList<ExecutableFlow> exFlows = new ArrayList<ExecutableFlow>();
 		int total = 0;
 		try {
-			total = executorManager.getExecutableFlows(project.getId(), flowId, from, length, exFlows);
-		} catch (ExecutorManagerException e) {
+			total = executorManager.getExecutableFlows(
+					project.getId(), flowId, from, length, exFlows);
+		}
+    catch (ExecutorManagerException e) {
 			ret.put("error", "Error retrieving executable flows");
 		}
 		
diff --git a/src/java/azkaban/webapp/servlet/ProjectServlet.java b/src/java/azkaban/webapp/servlet/ProjectServlet.java
index 68afdd7..ba263d6 100644
--- a/src/java/azkaban/webapp/servlet/ProjectServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectServlet.java
@@ -39,8 +39,10 @@ import azkaban.webapp.session.Session;
  * The main page
  */
 public class ProjectServlet extends LoginAbstractAzkabanServlet {
-	private static final Logger logger = Logger.getLogger(ProjectServlet.class.getName());
-	private static final String LOCKDOWN_CREATE_PROJECTS_KEY = "lockdown.create.projects";
+	private static final Logger logger = 
+			Logger.getLogger(ProjectServlet.class.getName());
+	private static final String LOCKDOWN_CREATE_PROJECTS_KEY = 
+			"lockdown.create.projects";
 	private static final long serialVersionUID = -1;
 
 	private UserManager userManager;
@@ -50,24 +52,23 @@ public class ProjectServlet extends LoginAbstractAzkabanServlet {
 	@Override
 	public void init(ServletConfig config) throws ServletException {
 		super.init(config);
-		
 		AzkabanWebServer server = (AzkabanWebServer)getApplication();
 
 		userManager = server.getUserManager();
-		lockdownCreateProjects = server.getServerProps().getBoolean(LOCKDOWN_CREATE_PROJECTS_KEY, false);
+		lockdownCreateProjects = server.getServerProps().getBoolean(
+				LOCKDOWN_CREATE_PROJECTS_KEY, false);
 		if (lockdownCreateProjects) {
 			logger.info("Creation of projects is locked down");
 		}
 	}
 	
 	@Override
-	protected void handleGet(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
-		
-		if(hasParam(req, "doaction")) {
-			if(getParam(req, "doaction").equals("search")) {
+	protected void handleGet(HttpServletRequest req, HttpServletResponse resp, 
+			Session session) throws ServletException, IOException {
+		if (hasParam(req, "doaction")) {
+			if (getParam(req, "doaction").equals("search")) {
 				String searchTerm = getParam(req, "searchterm");
-				
-				if(!searchTerm.equals("") && !searchTerm.equals(".*")) {
+				if (!searchTerm.equals("") && !searchTerm.equals(".*")) {
 					handleFilter(req, resp, session, searchTerm);
 					return;
 				}
@@ -76,8 +77,10 @@ public class ProjectServlet extends LoginAbstractAzkabanServlet {
 		
 		User user = session.getUser();
 
-		ProjectManager manager = ((AzkabanWebServer)getApplication()).getProjectManager();
-		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/index.vm");
+		ProjectManager manager = 
+				((AzkabanWebServer)getApplication()).getProjectManager();
+		Page page = newPage(
+				req, resp, session, "azkaban/webapp/servlet/velocity/index.vm");
 		
 		if (lockdownCreateProjects && !hasPermissionToCreateProject(user)) {
 			page.add("hideCreateProject", true);
@@ -85,21 +88,30 @@ public class ProjectServlet extends LoginAbstractAzkabanServlet {
 		
 		if (hasParam(req, "all")) {
 			List<Project> projects = manager.getProjects();
-			page.add("allProjects", "true");
+			page.add("viewProjects", "all");
+			page.add("projects", projects);
+		}
+		else if (hasParam(req, "group")) {
+			List<Project> projects = manager.getGroupProjects(user);
+			page.add("viewProjects", "group");
 			page.add("projects", projects);
 		}
 		else {
 			List<Project> projects = manager.getUserProjects(user);
+			page.add("viewProjects", "personal");
 			page.add("projects", projects);
 		}
 		
 		page.render();
 	}
 	
-	private void handleFilter(HttpServletRequest req, HttpServletResponse resp, Session session, String searchTerm) {
+	private void handleFilter(HttpServletRequest req, HttpServletResponse resp, 
+			Session session, String searchTerm) {
 		User user = session.getUser();
-		ProjectManager manager = ((AzkabanWebServer)getApplication()).getProjectManager();
-		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/index.vm");
+		ProjectManager manager = 
+				((AzkabanWebServer)getApplication()).getProjectManager();
+		Page page = newPage(
+				req, resp, session, "azkaban/webapp/servlet/velocity/index.vm");
 		if (hasParam(req, "all")) {
 			//do nothing special if one asks for 'ALL' projects
 			List<Project> projects = manager.getProjectsByRegex(searchTerm);
@@ -120,14 +132,14 @@ public class ProjectServlet extends LoginAbstractAzkabanServlet {
 	protected void handlePost(HttpServletRequest req, HttpServletResponse resp,
 			Session session) throws ServletException, IOException {
 		// TODO Auto-generated method stub
-		
 	}
 
 	private boolean hasPermissionToCreateProject(User user) {
-		for(String roleName: user.getRoles()) {
+		for (String roleName: user.getRoles()) {
 			Role role = userManager.getRole(roleName);
 			Permission perm = role.getPermission();
-			if (perm.isPermissionSet(Permission.Type.ADMIN) || perm.isPermissionSet(Permission.Type.CREATEPROJECTS)) {
+			if (perm.isPermissionSet(Permission.Type.ADMIN) || 
+					perm.isPermissionSet(Permission.Type.CREATEPROJECTS)) {
 				return true;
 			}
 		}
diff --git a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
index 693a707..cb6fe85 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
@@ -22,17 +22,23 @@
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
-		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.common.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.job.status.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.exflow.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.flow.job.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
-		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
+    <script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
+    
+    <script type="text/javascript" src="${context}/js/dust-full-2.2.3.min.js"></script>
+		<script type="text/javascript" src="${context}/js/flowstats.js"></script>
+		<script type="text/javascript" src="${context}/js/flowstats-no-data.js"></script>
+
+		<script type="text/javascript" src="${context}/js/azkaban/util/common.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/context-menu.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/job-status.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/layout.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/exflow.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/flow-stats.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/flow-job.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/svg-graph.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/svg-navigate.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -62,20 +68,20 @@
 		<div class="az-page-header">
 			<div class="container-full" id="flow-status">
         <div class="row">
-          <div class="col-lg-7">
+          <div class="header-title">
             <h1>
               <a href="${context}/executor?execid=${execid}">
                 Flow Execution <small>$execid <span id="flowStatus">-</span></small>
               </a>
             </h1>
           </div>
-          <div class="col-lg-5">
+          <div class="header-control">
             <div class="az-exflow-stats">
-              <div class="col-md-5">
+              <div class="col-xs-5">
                 <p><strong>Submit User</strong> <span id="submitUser">-</span></p>
                 <p><strong>Duration</strong> <span id="duration">-</span></p>
               </div>
-              <div class="col-md-7">
+              <div class="col-xs-7">
                 <p><strong>Start Time</strong> <span id="startTime">-</span></p>
                 <p><strong>End Time</strong> <span id="endTime">-</span></p>
               </div>
@@ -104,6 +110,7 @@
 				<li id="graphViewLink"><a href="#graph">Graph</a></li>
 				<li id="jobslistViewLink"><a href="#jobslist">Job List</a></li>
 				<li id="flowLogViewLink"><a href="#log">Flow Log</a></li>
+				<li id="statsViewLink"><a href="#stats">Stats</a></li>
 				<li class="nav-button pull-right"><button type="button" id="pausebtn" class="btn btn-primary">Pause</button></li>
 				<li class="nav-button pull-right"><button type="button" id="resumebtn" class="btn btn-primary">Resume</button></li>
 				<li class="nav-button pull-right"><button type="button" id="cancelbtn" class="btn btn-danger">Cancel</button></li>
@@ -120,7 +127,7 @@
 
     <div class="container-full" id="jobListView">
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
 					<table class="table table-striped table-bordered table-condensed table-hover executions-table">
 						<thead>
 							<tr>
@@ -136,7 +143,7 @@
 						<tbody id="executableBody">
 						</tbody>
 					</table>
-        </div><!-- /.col-lg-12 -->
+        </div><!-- /.col-xs-12 -->
       </div><!-- /.row -->
     </div><!-- /.container-full -->
 
@@ -144,7 +151,7 @@
 
     <div class="container-full container-fill" id="flowLogView">
 			<div class="row">
-				<div class="col-lg-12 col-content">
+				<div class="col-xs-12 col-content">
           <div class="log-viewer">
             <div class="panel panel-default">
               <div class="panel-heading">
@@ -158,9 +165,24 @@
               </div>
             </div><!-- /.panel -->
           </div><!-- /.log-viewer -->
-        </div><!-- /.col-lg-12 -->
+        </div><!-- /.col-xs-12 -->
       </div><!-- /.row -->
-    </div><!-- /. -->
+    </div><!-- /.container-full -->
+
+  ## Stats view.
+
+    <div class="container-full" id="statsView">
+      <div id="flow-stats-container">
+        <div class="row">
+          <div class="col-lg-12">
+            <div class="alert alert-default">
+              <h4>No stats available</h4>
+              <p>Stats for this flow execution are not available.</p>
+            </div>
+          </div>
+        </div>
+			</div><!-- /.row -->
+    </div><!-- /.container-fill -->
 	
 	## Error message message dialog.
 
diff --git a/src/java/azkaban/webapp/servlet/velocity/executionspage.vm b/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
index 91d1d0c..e089e75 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
@@ -21,7 +21,7 @@
 #parse("azkaban/webapp/servlet/velocity/style.vm")
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 	
-		<script type="text/javascript" src="${context}/js/azkaban.executions.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/executions.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -55,7 +55,7 @@
       </ul>
 
 			<div class="row" id="currently-running-view">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
           <table id="executingJobs" class="table table-striped table-bordered table-hover table-condensed executions-table">
             <thead>
               <tr>
@@ -98,11 +98,11 @@
 #end
             </tbody>
           </table>
-				</div><!-- /col-lg-12 -->
+				</div><!-- /col-xs-12 -->
 			</div><!-- /row -->
 
 			<div class="row" id="recently-finished-view">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
           <table id="recentlyFinished" class="table table-striped table-bordered table-hover table-condensed executions-table">
             <thead>
               <tr>
@@ -145,7 +145,7 @@
 #end	
             </tbody>
           </table>
-				</div><!-- /col-lg-12 -->
+				</div><!-- /col-xs-12 -->
 			</div><!-- /row -->
 		
 		</div><!-- /container-full -->
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
index cda497a..5c04043 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
@@ -14,12 +14,12 @@
  * the License.
 *#
 
-			<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
-			<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.common.utils.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.flow.execute.view.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/util/layout.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/util/svg-navigate.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/view/context-menu.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/util/common.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/view/svg-graph.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/view/flow-execute.js"></script>
 
 			<div class="modal modal-wide" id="execute-flow-panel">
 				<div class="modal-dialog">
@@ -29,7 +29,7 @@
 							<h4 class="modal-title" id="execute-flow-panel-title"></h4>
 						</div><!-- /modal-header -->
 						<div class="modal-body row">
-							<div class="col-md-4">
+							<div class="col-xs-4">
 								<ul class="nav nav-pills nav-stacked" id="graph-options">
 									<li id="flow-option" viewpanel="svg-div-custom">
 										<a href="#">Flow View</a>
@@ -52,8 +52,8 @@
 										<div class="menu-caption">Add temporary flow parameters that are used to override global settings for each job.</div>
 									</li>
 								</ul>
-							</div><!-- /col-md-4 -->
-							<div class="col-md-8">
+							</div><!-- /col-xs-4 -->
+							<div class="col-xs-8">
 								<div id="execution-graph-options-panel">
 
 ## SVG graph panel.
@@ -181,7 +181,7 @@
 									</div>
 
 								</div><!-- /execution-graph-options-panel -->
-							</div><!-- /col-md-8 -->
+							</div><!-- /col-xs-8 -->
 						</div><!-- /modal-body -->
 
 						<div class="modal-footer">
@@ -199,7 +199,7 @@
 #end
 *#
 							<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
-							<button type="button" class="btn btn-success" id="execute-btn">Execute</button>
+							<button type="button" class="btn btn-primary" id="execute-btn">Execute</button>
 						</div><!-- /modal-footer -->
 					</div><!-- /modal-content -->
 				</div><!-- /modal-dialog -->
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index 83dfff0..3ae46d6 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -23,18 +23,23 @@
 
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
 		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
+		<script type="text/javascript" src="${context}/js/d3.v3.min.js"></script>
 		
-    <script type="text/javascript" src="${context}/js/dust-core-2.2.2.min.js"></script>
+    <script type="text/javascript" src="${context}/js/dust-full-2.2.3.min.js"></script>
 		<script type="text/javascript" src="${context}/js/flowsummary.js"></script>
-
-		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.common.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.flow.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.flow.job.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.flow.graph.view.js"></script>
-		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
+		<script type="text/javascript" src="${context}/js/flowstats-no-data.js"></script>
+		<script type="text/javascript" src="${context}/js/flowstats.js"></script>
+
+		<script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/common.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/layout.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/time-graph.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/flow.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/flow-stats.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/flow-job.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/flow-graph.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/svg-navigate.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -47,6 +52,24 @@
 			var flowId = "${flowid}";
 			var execId = null;
 		</script>
+		<style>
+			.axis path,
+			.axis line {
+			  fill: none;
+			  stroke: #000;
+			  shape-rendering: crispEdges;
+			}
+			
+			.x.axis path {
+			  display: none;
+			}
+			
+			.line {
+			  fill: none;
+			  stroke: steelblue;
+			  stroke-width: 1.5px;
+			}
+		</style>
 		<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-svg.css" />
 		<link rel="stylesheet" type="text/css" href="${context}/css/bootstrap-datetimepicker.css" />
 	</head>
@@ -64,10 +87,10 @@
 		<div class="az-page-header">
 			<div class="container-full">
 				<div class="row">
-					<div class="col-lg-6">
+					<div class="col-xs-6">
 						<h1><a href="${context}/manager?project=${project.name}&flow=${flowid}">Flow <small>$flowid</small></a></h1>
 					</div>
-					<div class="col-lg-6">
+					<div class="col-xs-6">
 						<div class="pull-right az-page-header-form">
 							<button type="button" class="btn btn-sm btn-success" id="executebtn">Schedule / Execute Flow</button>
 						</div>
@@ -105,7 +128,11 @@
 
     <div class="container-full" id="executionsView">
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
+					<div class="well well-clear well-sm">
+            <div id="timeGraph"></div>
+          </div>
+
 					<table class="table table-striped table-bordered table-condensed table-hover" id="execTable">
 						<thead>
 							<tr>
@@ -137,10 +164,25 @@
 	## Summary view.
 
     <div class="container-full" id="summaryView">
-			<div class="row" id="summary-view-content">
-			</div><!-- /.row -->
+      <div id="summary-view-content">
+      </div>
+      <div id="flow-stats-container">
+        <div class="row">
+          <div class="col-xs-12">
+            <div class="alert alert-info">
+              <h4>Analyze last run</h4>
+              <p>Analyze the last run for aggregate performance statistics. <strong>Note:</strong> this may take a few minutes, especially if your flow is large.</p>
+              <p>
+                <button type="button" id="analyze-btn" class="btn btn-primary">Analyze</button>
+              </p>
+            </div>
+          </div>
+        </div><!-- /.col-lg-12 -->
+      </div>
     </div><!-- /.container-fill -->
 
+  ## Context menu and the rest of the page.
+
     <div class="container-full">
 			<div id="contextMenu">
 			</div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/historypage.vm b/src/java/azkaban/webapp/servlet/velocity/historypage.vm
index 63db454..3df93ad 100644
--- a/src/java/azkaban/webapp/servlet/velocity/historypage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/historypage.vm
@@ -25,7 +25,7 @@
 
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
 		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.history.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/history.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -46,10 +46,10 @@
 		<div class="az-page-header">
       <div class="container-full">
         <div class="row">
-          <div class="col-lg-6">
+          <div class="header-title">
             <h1><a href="${context}/history">History</a></h1>
           </div>
-          <div class="col-lg-6">
+          <div class="header-control">
             <form id="search-form" method="get" class="form-inline az-page-header-form" role="form">
               <input type="hidden" name="search" value="true">
               <div class="form-group">
@@ -72,7 +72,7 @@
   #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
 
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
           <table id="executingJobs" class="table table-striped table-bordered table-hover table-condensed executions-table">
             <thead>
               <tr>
@@ -144,7 +144,7 @@
 						<li id="next"><a href="${context}/history?page=${next.page}&size=${next.size}">Next<span class="arrow">&rarr;</span></a></li>
   #end
 					</ul>
-				</div><!-- /col-lg-12 -->
+				</div><!-- /col-xs-12 -->
 			</div><!-- /row -->
 
   ## Advanced Filter Modal.
diff --git a/src/java/azkaban/webapp/servlet/velocity/index.vm b/src/java/azkaban/webapp/servlet/velocity/index.vm
index 159bd1a..9cbe273 100644
--- a/src/java/azkaban/webapp/servlet/velocity/index.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/index.vm
@@ -21,8 +21,8 @@
 #parse("azkaban/webapp/servlet/velocity/style.vm")
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 
-		<script type="text/javascript" src="${context}/js/azkaban.table.sort.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.main.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/table-sort.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/main.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -41,20 +41,16 @@
 		<div class="az-page-header">
       <div class="container-full">
         <div class="row">
-          <div class="col-lg-6">
-#if ($allProjects)
-            <h1><a href="${context}/index">All Projects</a></h1>
-#else
-            <h1><a href="${context}/index">My Projects</a></h1>
-#end
+          <div class="header-title">
+            <h1><a href="${context}/index">Projects</a></h1>
           </div>
-          <div class="col-lg-6">
+          <div class="header-control">
             <form id="search-form" method="get" class="form-inline az-page-header-form" role="form">
               <input type="hidden" name="doaction" value="search">
-#if ($allProjects)
+#if ($viewProjects == 'all')
               <input type="hidden" name="all" value="true">				
 #end
-              <div class="form-group col-md-9">
+              <div class="form-group col-xs-9">
                 <div class="input-group">
                   <input id="search-textbox" type="text" placeholder="Project name containing..." value=#if($search_term) ${search_term} #else "" #end class="form-control input-sm" name="searchterm">
                   <span class="input-group-btn">
@@ -63,7 +59,7 @@
                 </div>
               </div>
 #if (!$hideCreateProject)
-              <div class="form-group col-md-3" id="create-project">
+              <div class="form-group col-xs-3" id="create-project">
 ## Note: The Create Project button is not completely flush to the right because
 ## form-group has padding.
                 <div class="pull-right">
@@ -83,50 +79,50 @@
 
 ## Table of projects.
 
-			<div class="row">
-				<div class="col-lg-12">
-          <table class="table table-condensed table-striped table-bordered table-hover" id="all-jobs">
-            <thead>
-              <tr>
-                <th class="tb-name">Name</th>
-                <th class="tb-up-date">Modified Date</th>
-                <th class="tb-owner">Modified By</th>
-              </tr>
-            </thead>
-            <tbody>
+			<div class="row row-offcanvas row-offcanvas-left">
+        <div class="col-xs-6 col-sm-3 sidebar-offcanvas graph-sidebar">
+          <ul class="nav nav-pills nav-stacked">
+            <li#if ($viewProjects == 'personal') class="active"#end><a href="${context}/index">Personal</a></li>
+            <li#if ($viewProjects == 'group') class="active"#end><a href="${context}/index?group">Group</a></li>
+            <li#if ($viewProjects == 'all') class="active"#end><a href="${context}/index?all">All</a></li>
+          </ul>
+        </div>
+				<div class="col-xs-12 col-sm-9 col-content">
 #if (!$projects.isEmpty())
+          <ul id="project-list">
 	#foreach ($project in $projects)
-              <tr class="az-project-row">
-                <td id="${project.name}" class="tb-name project-expand expanded">
-                  <span class="state-icon state-icon-expand az-expander"></span>
-                  <a href="${context}/manager?project=${project.name}">$project.name</a>
-                </td>
-                <td class="tb-up-date">$utils.formatDate($project.lastModifiedTimestamp)</td>
-                <td class="tb-owner">$project.lastModifiedUser</td>
-              </tr>
-              <tr class="childrow collapse" id="${project.name}-child">
-                <td colspan="3">
-                  <table class="table table-bordered">
-                    <thead>
-                      <tr>
-                        <th class="tb-name">Flows</th>
-                      </tr>
-                    </thead>
-                    <tbody id="${project.name}-tbody">
-                    </tbody>
-                  </table>
-                </td>
-              </tr>
-	#end
+            <li>
+              <div class="project-info">
+                <h4><a href="${context}/manager?project=${project.name}">$project.name</a></h4>
+                <p class="project-description">$project.description</p>
+                <p class="project-last-modified">Last modified on <strong>$utils.formatDate($project.lastModifiedTimestamp)</strong> by <strong>$project.lastModifiedUser</strong>.</p>
+              </div>
+              <div class="project-expander" id="${project.name}">
+                <span class="glyphicon glyphicon-chevron-down project-expander-icon"></span>
+              </div>
+              <div class="clearfix"></div>
+              <div class="project-flows" id="${project.name}-child">
+                <table class="table">
+                  <thead>
+                    <tr>
+                      <th class="tb-name">Flows</th>
+                    </tr>
+                  </thead>
+                  <tbody id="${project.name}-tbody">
+                  </tbody>
+                </table>
+              </div>
+            </li>
+  #end
+          </ul>
 #else
-              <tr>
-                <td colspan="3">No viewable projects found.</td>
-              </tr>
+          <div class="alert alert-default">
+            <h4>No Viewable Projects</h4>
+            <p>Click Create Project to create a new project.</p>
+          </div>
 #end
-            </tbody>
-          </table>
-				</div>
-			</div>
+        </div>
+      </div>
 
 ## Modal dialog to be displayed to create a new project.
 
diff --git a/src/java/azkaban/webapp/servlet/velocity/javascript.vm b/src/java/azkaban/webapp/servlet/velocity/javascript.vm
index 2b2c316..2ebb28b 100644
--- a/src/java/azkaban/webapp/servlet/velocity/javascript.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/javascript.vm
@@ -17,5 +17,5 @@
 		<script type="text/javascript" src="${context}/js/jquery/jquery-1.9.1.js"></script>    
 		<script type="text/javascript" src="${context}/js/bootstrap.min.js"></script>    
 		<script type="text/javascript" src="${context}/js/underscore-1.4.4-min.js"></script>
-		<script type="text/javascript" src="${context}/js/namespace.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/namespace.js"></script>
 		<script type="text/javascript" src="${context}/js/backbone-0.9.10-min.js"></script>
diff --git a/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm b/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm
index 30c8f74..03a1806 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm
@@ -21,7 +21,7 @@
 #parse ("azkaban/webapp/servlet/velocity/style.vm")
 #parse ("azkaban/webapp/servlet/velocity/javascript.vm")
 
-		<script type="text/javascript" src="${context}/js/azkaban.jmx.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/jmx.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -51,7 +51,7 @@
   ## Web Client JMX 
 
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
 					<div class="panel panel-default">
 						<div class="panel-heading">Web Client JMX</div>
 						<table id="all-jmx" class="table table-condensed table-bordered table-striped table-hover">
@@ -101,7 +101,7 @@
 			
   #foreach ($executor in $executorRemoteMBeans.entrySet())
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
 					<div class="panel panel-default">
 						<div class="panel-heading">Remote Executor JMX $executor.key</div>
 						<table class="remoteJMX table table-striped table-condensed table-bordered table-hover">
@@ -150,7 +150,7 @@
 			
   #foreach ($triggerserver in $triggerserverRemoteMBeans.entrySet())
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
 					<div class="panel panel-default">
 						<div class="panel-heading">Remote Trigger Server JMX $triggerserver.key</div>
 						<table class="remoteJMX table table-condensed table-striped table-bordered table-hover">
diff --git a/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm b/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm
index a8df9c2..0a88c63 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm
@@ -21,8 +21,9 @@
 #parse("azkaban/webapp/servlet/velocity/style.vm")
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 
-		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.jobdetails.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/model/log-data.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/job-details.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -51,10 +52,10 @@
 		<div class="az-page-header">
 			<div class="container-full">
 				<div class="row">
-					<div class="col-lg-6">
+					<div class="col-xs-6">
 						<h1><a href="${context}/executor?execid=${execid}&job=${jobid}">Job Execution <small>$jobid</small></a></h1>
 					</div>
-					<div class="col-lg-6">
+					<div class="col-xs-6">
 						<div class="pull-right az-page-header-form">
 							<a href="${context}/manager?project=${projectName}&flow=${flowid}&job=$jobid" class="btn btn-info">Job Properties</a>
 						</div>
@@ -89,7 +90,7 @@
 
     <div class="container-full container-fill" id="jobLogView">
 			<div class="row">
-				<div class="col-lg-12 col-content">
+				<div class="col-xs-12 col-content">
           <div class="log-viewer">
             <div class="panel panel-default">
               <div class="panel-heading">
@@ -110,44 +111,54 @@
 	## Job Summary
 
     <div class="container-full" id="jobSummaryView">
-			<div class="row">
-				<div class="col-lg-12">
-					<table id="commandTable" class="table table-striped table-bordered table-hover">
-					</table>
-				
-					<div class="panel panel-default" id="jobsummary">
-						<div class="panel-heading">Job Summary</div>
-						<table class="table table-striped table-bordered table-hover">
-							<thead id="summaryHeader">
-							</thead>
-							<tbody id="summaryBody">
-							</tbody>
-						</table>
-					</div>
-				
-					<div class="panel panel-default" id="jobstats">
-						<div class="panel-heading">Job Stats</div>
-            <div class="panel-body panel-body-stats">
-              <table class="table table-striped table-bordered table-hover table-condensed">
-                <thead id="statsHeader">
-                </thead>
-                <tbody id="statsBody">
-                </tbody>
-              </table>
+      <div class="row">
+        <div class="col-lg-12">
+          <h3>
+            Job Summary
+            <div class="pull-right">
+              <button type="button" id="updateSummaryBtn" class="btn btn-xs btn-default">Refresh</button>
             </div>
-					</div>
-					
-					<div class="panel panel-default" id="hiveTable">
-						<div class="panel-heading">Job Summary</div>
-						<table class="table table-striped table-bordered table-hover">
-							<thead id="hiveTableHeader">
-							</thead>
-							<tbody id="hiveTableBody">
-							</tbody>
-						</table>
-					</div>
-				</div>
-			</div>
+          </h3>
+
+          <div id="command-summary">
+            <h4>Command Summary</h4>
+            <table id="commandTable" class="table table-striped table-bordered table-hover">
+            </table>
+          </div>
+        
+          <div id="pigJobSummary">
+            <h4>Pig Job Summary</h4>
+            <table class="table table-striped table-bordered table-hover">
+              <thead id="summaryHeader">
+              </thead>
+              <tbody id="summaryBody">
+              </tbody>
+            </table>
+          </div>
+        
+          <div id="pigJobStats">
+            <h4>Pig Job Stats</h4>
+              <div class="panel-body-stats">
+                <table class="table table-striped table-bordered table-hover table-condensed">
+                  <thead id="statsHeader">
+                  </thead>
+                  <tbody id="statsBody">
+                  </tbody>
+                </table>
+            </div>
+          </div>
+
+          <div id="hiveJobSummary">
+            <h4>Hive Job Summary</h4>
+            <table class="table table-striped table-bordered table-hover" id="hiveTable">
+              <thead id="hiveTableHeader">
+              </thead>
+              <tbody id="hiveTableBody">
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
     </div>
 			
 	## Error message message dialog.
diff --git a/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm b/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm
index 0177383..48b7394 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm
@@ -22,8 +22,9 @@
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 
 		<script type="text/javascript" src="${context}/js/d3.v3.min.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.jobhistory.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/time-graph.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/job-history.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -86,7 +87,7 @@
   ## Time graph and job history table.
 
 			<div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
 					<div class="well well-clear well-sm">
             <div id="timeGraph"></div>
           </div>
@@ -152,7 +153,7 @@
 						<li id="next"><a href="${context}/manager?project=${projectName}&job=${jobid}&history&page=${next.page}&size=${next.size}">Next<span class="arrow">&rarr;</span></a></li>
 					</ul>
 
-				</div><!-- /.col-lg-12 -->
+				</div><!-- /.col-xs-12 -->
 			</div><!-- /.row -->
 
 		</div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/jobpage.vm b/src/java/azkaban/webapp/servlet/velocity/jobpage.vm
index 5dbec1f..e212556 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jobpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jobpage.vm
@@ -21,7 +21,7 @@
 #parse ("azkaban/webapp/servlet/velocity/style.vm")
 #parse ("azkaban/webapp/servlet/velocity/javascript.vm")
 
-		<script type="text/javascript" src="${context}/js/azkaban.jobedit.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/job-edit.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -47,10 +47,10 @@
 		<div class="az-page-header">
 			<div class="container-full">
         <div class="row">
-          <div class="col-lg-6">
+          <div class="col-xs-6">
             <h1><a href="${context}/manager?project=${project.name}&flow=${flowid}&job=${jobid}">Job <small>$jobid</small></a></h1>
           </div>
-          <div class="col-lg-6">
+          <div class="col-xs-6">
             <div class="pull-right az-page-header-form">
               <a href="${context}/manager?project=${project.name}&job=$jobid&history" class="btn btn-info btn-sm">History</a>
             </div>
@@ -102,7 +102,7 @@
 							</tbody>
 						</table>
 					</div>
-				</div><!-- /col-lg-8 -->
+				</div><!-- /col-xs-8 -->
 				<div class="col-xs-6 col-sm-3 sidebar-offcanvas">
 					<div class="well" id="job-summary">
 						<h3>Job <small>$jobid</small></h3>
@@ -158,7 +158,7 @@
 	#end
 						</ul>
 					</div><!-- /panel -->
-				</div><!-- /col-lg-4 -->
+				</div><!-- /col-xs-4 -->
 			</div><!-- /row -->
 
 ## Edit job modal.
diff --git a/src/java/azkaban/webapp/servlet/velocity/login.vm b/src/java/azkaban/webapp/servlet/velocity/login.vm
index bba573e..0067a02 100644
--- a/src/java/azkaban/webapp/servlet/velocity/login.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/login.vm
@@ -21,7 +21,7 @@
 #parse("azkaban/webapp/servlet/velocity/style.vm")
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 
-    <script type="text/javascript" src="${context}/js/azkaban.login.js"></script>
+    <script type="text/javascript" src="${context}/js/azkaban/view/login.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 		</script>
diff --git a/src/java/azkaban/webapp/servlet/velocity/messagedialog.vm b/src/java/azkaban/webapp/servlet/velocity/messagedialog.vm
index 3068df5..ecf86e0 100644
--- a/src/java/azkaban/webapp/servlet/velocity/messagedialog.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/messagedialog.vm
@@ -14,7 +14,7 @@
  * the License.
 *#
 
-			<script type="text/javascript" src="${context}/js/azkaban.message.dialog.view.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/view/message-dialog.js"></script>
 			
 			<div class="modal" id="azkaban-message-dialog">
 				<div class="modal-dialog">
diff --git a/src/java/azkaban/webapp/servlet/velocity/nav.vm b/src/java/azkaban/webapp/servlet/velocity/nav.vm
index f709390..91e7e66 100644
--- a/src/java/azkaban/webapp/servlet/velocity/nav.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/nav.vm
@@ -22,13 +22,6 @@
     <div class="navbar navbar-inverse navbar-static-top">
       <div class="container-full">
         <div class="navbar-header">
-#if ($navbar_disabled != 1)
-          <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
-            <span class="icon-bar"></span>
-            <span class="icon-bar"></span>
-            <span class="icon-bar"></span>
-          </button>
-#end
 					<div class="navbar-logo">
 						<a href="${context}/">Azkaban</a>
 					</div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm b/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm
index 43d29c4..067b89c 100644
--- a/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm
@@ -21,8 +21,8 @@
 #parse ("azkaban/webapp/servlet/velocity/style.vm")
 #parse ("azkaban/webapp/servlet/velocity/javascript.vm")
 
-		<script type="text/javascript" src="${context}/js/azkaban.permission.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.projectmodals.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/project-permissions.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/project-modals.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -232,10 +232,10 @@
             </table>
           </div>
 	
-				</div><!-- /col-lg-8 -->
+				</div><!-- /col-xs-8 -->
 				<div class="col-xs-6 col-sm-3 sidebar-offcanvas">
 	#parse ("azkaban/webapp/servlet/velocity/projectsidebar.vm")
-				</div><!-- /col-lg-4 -->
+				</div><!-- /col-xs-4 -->
 			</div><!-- /row -->
 
 ## Remove proxy user modal dialog.
diff --git a/src/java/azkaban/webapp/servlet/velocity/projectlogpage.vm b/src/java/azkaban/webapp/servlet/velocity/projectlogpage.vm
index dbb998a..08dbb88 100644
--- a/src/java/azkaban/webapp/servlet/velocity/projectlogpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/projectlogpage.vm
@@ -21,10 +21,10 @@
 #parse ("azkaban/webapp/servlet/velocity/style.vm")
 #parse ("azkaban/webapp/servlet/velocity/javascript.vm")
 
-		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.projectlog.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.projectmodals.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/project-logs.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/project-modals.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
diff --git a/src/java/azkaban/webapp/servlet/velocity/projectpage.vm b/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
index 8d6e865..e6b5314 100644
--- a/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
@@ -25,9 +25,9 @@
 		<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-svg.css" />
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
 		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.project.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.projectmodals.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/project.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/project-modals.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -85,17 +85,17 @@
             </div>
 		#end
 	#else
-            <div class="alert alert-info">
+            <div class="alert alert-default">
               <h4>No Flows</h4>
               <p>No flows have been uploaded to this project yet.</p>
             </div>
 	#end
           </div><!-- /#flow-tabs -->
-				</div><!-- /col-lg-8 -->
+				</div><!-- /col-xs-8 -->
 
 				<div class="col-xs-6 col-sm-3 sidebar-offcanvas">
 	#parse ("azkaban/webapp/servlet/velocity/projectsidebar.vm")
-				</div><!-- /col-lg-4 -->
+				</div><!-- /col-xs-4 -->
 			</div><!-- /row -->
 
 	#parse ("azkaban/webapp/servlet/velocity/projectmodals.vm")
diff --git a/src/java/azkaban/webapp/servlet/velocity/projectpageheader.vm b/src/java/azkaban/webapp/servlet/velocity/projectpageheader.vm
index 0936995..6bb0858 100644
--- a/src/java/azkaban/webapp/servlet/velocity/projectpageheader.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/projectpageheader.vm
@@ -17,7 +17,7 @@
 		<div class="az-page-header">
 			<div class="container-full">
         <div class="row">
-          <div class="col-lg-6" id="project-page-header">
+          <div class="header-title" id="project-page-header">
             <h1><a href="${context}/manager?project=${project.name}">Project <small>$project.name</small></a></h1>
             <p class="editable" id="project-description">$project.description</p>
             <div id="project-description-form" class="editable-form">
@@ -29,7 +29,7 @@
               </div>
             </div>
           </div>
-          <div class="col-lg-6">
+          <div class="header-control">
             <div class="pull-right az-page-header-form" id="project-options">
               <button id="project-delete-btn" class="btn btn-sm btn-danger">
                 <span class="glyphicon glyphicon-trash"></span> Delete Project
diff --git a/src/java/azkaban/webapp/servlet/velocity/propertypage.vm b/src/java/azkaban/webapp/servlet/velocity/propertypage.vm
index c1d3097..9ddfeb5 100644
--- a/src/java/azkaban/webapp/servlet/velocity/propertypage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/propertypage.vm
@@ -86,7 +86,7 @@
 							</tbody>
 						</table>
 					</div>
-				</div><!-- /col-lg-8 -->
+				</div><!-- /col-xs-8 -->
 				<div class="col-xs-6 col-sm-3 sidebar-offcanvas">
 					<div class="well" id="job-summary">
 						<h4>Properties <small>$property</small></h4>
diff --git a/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm b/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
index 761350d..d5a37ca 100644
--- a/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
@@ -28,10 +28,11 @@
 		<script type="text/javascript" src="${context}/js/jquery/jquery.svg.min.js"></script>    
 		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-timepicker-addon.js"></script> 
 		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-sliderAccess.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.table.sort.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.schedule.svg.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
-		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
+
+		<script type="text/javascript" src="${context}/js/azkaban/view/table-sort.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/schedule-svg.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/context-menu.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/util/svg-navigate.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -60,7 +61,7 @@
 
     <div class="container-full">
       <div class="row">
-        <div class="col-lg-12">
+        <div class="col-xs-12">
           <div class="pull-right">
             <button type="button" class="nav-prev-week btn btn-default">Previous Week</button>
             <button type="button" class="nav-this-week btn btn-default">Today</button>
diff --git a/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
index 87285cf..1aa2610 100644
--- a/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
@@ -25,8 +25,8 @@
 		
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
 		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.table.sort.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.scheduled.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/table-sort.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/scheduled.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -59,7 +59,7 @@
   #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
       
       <div class="row">
-				<div class="col-lg-12">
+				<div class="col-xs-12">
           <table id="scheduledFlowsTbl" class="table table-striped table-condensed table-bordered table-hover">
             <thead>
               <tr>
@@ -102,7 +102,7 @@
 	#end
             </tbody>
           </table>
-				</div><!-- /col-lg-12 -->
+				</div><!-- /col-xs-12 -->
 			</div><!-- /row -->
 
   ## Set SLA modal.
diff --git a/src/java/azkaban/webapp/servlet/velocity/schedulepanel.vm b/src/java/azkaban/webapp/servlet/velocity/schedulepanel.vm
index 3381d91..3e39a5c 100644
--- a/src/java/azkaban/webapp/servlet/velocity/schedulepanel.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/schedulepanel.vm
@@ -14,8 +14,8 @@
  * the License.
 *#
 
-			<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>  
-			<script type="text/javascript" src="${context}/js/azkaban.schedule.panel.view.js"></script>
+			<script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>  
+			<script type="text/javascript" src="${context}/js/azkaban/view/schedule-panel.js"></script>
 			
 			<div class="modal" id="schedule-modal">
 				<div class="modal-dialog">
diff --git a/src/java/azkaban/webapp/servlet/velocity/triggerspage.vm b/src/java/azkaban/webapp/servlet/velocity/triggerspage.vm
index 542e699..c48ec9e 100644
--- a/src/java/azkaban/webapp/servlet/velocity/triggerspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/triggerspage.vm
@@ -27,8 +27,9 @@
 		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-1.10.1.custom.js"></script>
 		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-timepicker-addon.js"></script> 
 		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-sliderAccess.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.table.sort.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.triggers.view.js"></script>
+
+		<script type="text/javascript" src="${context}/js/azkaban/view/table-sort.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/view/triggers.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -57,7 +58,7 @@
   #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
 
 			<div class="row">
-        <div class="col-lg-12">
+        <div class="col-xs-12">
           <table id="triggersTbl" class="table table-striped table-bordered table-condensed table-hover">
             <thead>
               <tr>
diff --git a/src/less/.gitignore b/src/less/.gitignore
new file mode 100644
index 0000000..2416a67
--- /dev/null
+++ b/src/less/.gitignore
@@ -0,0 +1 @@
+obj/
diff --git a/src/less/azkaban.less b/src/less/azkaban.less
index da43720..54ba380 100644
--- a/src/less/azkaban.less
+++ b/src/less/azkaban.less
@@ -1,10 +1,12 @@
+@import "non-responsive.less";
+
 @import "base.less";
-@import "offcanvas.less";
+@import "off-canvas.less";
 
 @import "navbar.less";
 @import "header.less";
 
-@import "contextmenu.less";
+@import "context-menu.less";
 @import "tables.less";
 
 @import "login.less";

src/less/base.less 16(+16 -0)

diff --git a/src/less/base.less b/src/less/base.less
index 573ccc1..cb7a0da 100644
--- a/src/less/base.less
+++ b/src/less/base.less
@@ -2,6 +2,8 @@
   padding: 0 105px;
   margin: 0 auto;
   width: 100%;
+  max-width: none;
+  min-width: 1075px;
 }
 
 .container-fill {
@@ -22,6 +24,20 @@
   }
 }
 
+.alert-default {
+  color: #a0a0a0;
+  background-color: #f5f5f5;
+  border-color: #dddddd;
+
+  hr {
+    border-top-color: #cccccc;
+  }
+
+  .alert-link {
+    color: #a0a0a0;
+  }
+}
+
 // Wide modal used for certain panels such as executing flow panel.
 .modal-wide .modal-dialog {
   width: 80%;

src/less/flow.less 36(+27 -9)

diff --git a/src/less/flow.less b/src/less/flow.less
index 931f47c..4c23e39 100644
--- a/src/less/flow.less
+++ b/src/less/flow.less
@@ -25,7 +25,7 @@
 .flow-progress-bar {
 	height: 100%;
   background-color: #ccc;
-  border-radius: 4px;
+  border-radius: 5px;
   -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
           box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
   -webkit-transition: width 0.6s ease;
@@ -84,7 +84,8 @@ td {
       background-color: #c82123;
     }
 
-    &.READY {
+    &.READY,
+    &.UNKNOWN {
       background-color: #ccc;
     }
 
@@ -96,10 +97,7 @@ td {
       background-color: #f19153;	
     }
 
-    &.DISABLED {
-      background-color: #aaa;	
-    }
-
+    &.DISABLED,
     &.SKIPPED {
       background-color: #aaa;	
     }
@@ -107,10 +105,30 @@ td {
     &.KILLED {
       background-color: #d9534f;
     }
+  }
+}
 
-    &.UNKNOWN {
-      background-color: #ccc;
-    }
+#flowStatus {
+  &.SKIPPED {
+    color: #aaa;
+  }
+
+  &.SUCCEEDED {
+    color: #4e911e;
+  }
+
+  &.RUNNING {
+    color: #009fc9;
+  }
+
+  &.PAUSED {
+    color: #c92123;
+  }
+
+  &.FAILED,
+  &.FAILED_FINISHING,
+  &.KILLED {
+    color: #cc0000;
   }
 }
 
diff --git a/src/less/header.less b/src/less/header.less
index 3ca6d00..9ec9c27 100644
--- a/src/less/header.less
+++ b/src/less/header.less
@@ -35,4 +35,20 @@
   .editable-form {
     display: none;
   }
+
+  .header-title {
+    padding-left: 15px;
+    float: left;
+    width: 40%;
+  }
+
+  .header-control {
+    float: right;
+    padding-right: 15px;
+    width: 500px;
+
+    .form-group {
+      padding: 0;
+    }
+  }
 }

src/less/Makefile 16(+16 -0)

diff --git a/src/less/Makefile b/src/less/Makefile
new file mode 100644
index 0000000..48f04c6
--- /dev/null
+++ b/src/less/Makefile
@@ -0,0 +1,16 @@
+OBJ_DIR = obj
+OBJ = \
+	$(OBJ_DIR)/azkaban.css \
+	$(OBJ_DIR)/azkaban-svg.css
+
+all: $(OBJ)
+
+$(OBJ_DIR)/%.css: %.less
+	lessc $< $@
+
+clean:
+	rm -rf $(OBJ_DIR)
+
+.SUFFIXES: .less .css
+
+.PHONY: all clean
diff --git a/src/less/non-responsive.less b/src/less/non-responsive.less
new file mode 100644
index 0000000..7fa282c
--- /dev/null
+++ b/src/less/non-responsive.less
@@ -0,0 +1,88 @@
+/* Non-responsive overrides
+ *
+ * Utilitze the following CSS to disable the responsive-ness of the container,
+ * grid system, and navbar.
+ */
+
+/* Reset the container */
+.container {
+  max-width: none !important;
+  width: 970px;
+}
+
+.container .navbar-header,
+.container .navbar-collapse {
+  margin-right: 0;
+  margin-left: 0;
+}
+
+/* Always float the navbar header */
+.navbar-header {
+  float: left;
+}
+
+/* Undo the collapsing navbar */
+.navbar-collapse {
+  display: block !important;
+  height: auto !important;
+  padding-bottom: 0;
+  overflow: visible !important;
+}
+
+.navbar-toggle {
+  display: none;
+}
+.navbar-collapse {
+  border-top: 0;
+}
+
+.navbar-brand {
+  margin-left: -15px;
+}
+
+/* Always apply the floated nav */
+.navbar-nav {
+  float: left;
+  margin: 0;
+}
+.navbar-nav > li {
+  float: left;
+}
+.navbar-nav > li > a {
+  padding: 15px;
+}
+
+/* Redeclare since we override the float above */
+.navbar-nav.navbar-right {
+  float: right;
+}
+
+/* Undo custom dropdowns */
+.navbar .navbar-nav .open .dropdown-menu {
+  position: absolute;
+  float: left;
+  background-color: #fff;
+  border: 1px solid #cccccc;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-width: 0 1px 1px;
+  border-radius: 0 0 4px 4px;
+  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+          box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+}
+.navbar-default .navbar-nav .open .dropdown-menu > li > a {
+  color: #333;
+}
+.navbar .navbar-nav .open .dropdown-menu > li > a:hover,
+.navbar .navbar-nav .open .dropdown-menu > li > a:focus,
+.navbar .navbar-nav .open .dropdown-menu > .active > a,
+.navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
+.navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
+  color: #fff !important;
+  background-color: #428bca !important;
+}
+.navbar .navbar-nav .open .dropdown-menu > .disabled > a,
+.navbar .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+.navbar .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+  color: #999 !important;
+  background-color: transparent !important;
+}
diff --git a/src/less/project.less b/src/less/project.less
index b3ae43f..d7e1fb9 100644
--- a/src/less/project.less
+++ b/src/less/project.less
@@ -1,5 +1,55 @@
-.az-project-row {
-	cursor: pointer;
+#project-list {
+  padding: 0;
+  margin: 0;
+
+  li {
+    list-style: none;
+    border-bottom: 1px solid #cccccc;
+    padding-top: 14px;
+    padding-bottom: 0px;
+    &:first-child {
+      border-top: 1px solid #cccccc;
+    }
+  }
+
+  .project-expander {
+    float: right;
+    cursor: pointer;
+    &:hover {
+      color: #2a6496;
+    }
+  }
+
+  .project-info {
+    float: left;
+    h4 {
+      margin-top: 0;
+      margin-bottom: 4px;
+    }
+    
+    .project-description {
+      margin-bottom: 4px;
+    }
+
+    .project-last-modified {
+      color: #a0a0a0;
+      margin-bottom: 16px;
+      strong {
+        font-weight: normal;
+        color: #000000;
+      }
+    }
+  }
+
+  .project-flows {
+    display: none;
+    background-color: #f9f9f9;
+    table {
+      background: transparent;
+      margin-bottom: 0;
+      border-top: 1px solid #dddddd;
+    }
+  }
 }
 
 // Flow panel heading.
diff --git a/src/package/execserver/conf/azkaban.properties b/src/package/execserver/conf/azkaban.properties
index b54b0f5..3f9beda 100644
--- a/src/package/execserver/conf/azkaban.properties
+++ b/src/package/execserver/conf/azkaban.properties
@@ -8,6 +8,8 @@ azkaban.jobtype.plugin.dir=plugins/jobtypes
 executor.global.properties=conf/global.properties
 azkaban.project.dir=projects
 
+azkaban.stats.dir=
+
 database.type=mysql
 mysql.port=3306
 mysql.host=localhost
diff --git a/src/package/soloserver/conf/azkaban.properties b/src/package/soloserver/conf/azkaban.properties
index 7524a14..d600ed9 100644
--- a/src/package/soloserver/conf/azkaban.properties
+++ b/src/package/soloserver/conf/azkaban.properties
@@ -20,6 +20,9 @@ database.type=h2
 h2.path=data/azkaban
 h2.create.tables=true
 
+# Stats
+azkaban.stats.dir=
+
 # Velocity dev mode
 velocity.dev.mode=false
 
diff --git a/src/package/webserver/bin/schedule2trigger.sh b/src/package/webserver/bin/schedule2trigger.sh
new file mode 100644
index 0000000..1178cf3
--- /dev/null
+++ b/src/package/webserver/bin/schedule2trigger.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+java -cp "lib/*:extlib/*" azkaban.migration.schedule2trigger.Schedule2Trigger conf/azkaban.properties
+
diff --git a/src/package/webserver/conf/azkaban.properties b/src/package/webserver/conf/azkaban.properties
index 3ccb2f3..3fe43a0 100644
--- a/src/package/webserver/conf/azkaban.properties
+++ b/src/package/webserver/conf/azkaban.properties
@@ -22,6 +22,8 @@ mysql.user=azkaban
 mysql.password=azkaban
 mysql.numconnections=100
 
+azkaban.stats.dir=
+
 # Velocity dev mode
 velocity.dev.mode=false
 
diff --git a/src/tl/.gitignore b/src/tl/.gitignore
new file mode 100644
index 0000000..2416a67
--- /dev/null
+++ b/src/tl/.gitignore
@@ -0,0 +1 @@
+obj/

src/tl/flowstats.tl 133(+133 -0)

diff --git a/src/tl/flowstats.tl b/src/tl/flowstats.tl
new file mode 100644
index 0000000..8fffebe
--- /dev/null
+++ b/src/tl/flowstats.tl
@@ -0,0 +1,133 @@
+      <div class="row">
+        <div class="col-xs-12">
+          <h4>Resources</h4>
+          <table class="table table-bordered table-condensed table-striped">
+            <thead>
+              <tr>
+                <th class="property-key">Resource</th>
+                <th class="property-key">Value</th>
+                <th>Job Name</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td class="property-key">Max Map Slots</td>
+                <td>{stats.mapSlots.max}</td>
+                <td>{stats.mapSlots.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max Reduce Slots</td>
+                <td>{stats.reduceSlots.max}</td>
+                <td>{stats.reduceSlots.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Total Map Slots</td>
+                <td colspan="2">{stats.totalMapSlots}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Total Reduce Slots</td>
+                <td colspan="2">{stats.totalReduceSlots}</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      <div class="row">
+        <div class="col-xs-12">
+          <h4>Parameters</h4>
+          <table class="table table-bordered table-condensed table-striped">
+            <thead>
+              <tr>
+                <th class="property-key">Parameter</th>
+                <th class="property-key">Value</th>
+                <th>Job Name</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td class="property-key">Max <code>-Xmx</code></td>
+                <td>{stats.xmx.str}</td>
+                <td>{stats.xmx.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max <code>-Xms</code></td>
+                {?stats.xms.set}
+                <td>
+                  {stats.xms.str}
+                </td>
+                <td>
+                  {stats.xms.job}
+                </td>
+                {:else}
+                <td colspan="2">
+                  Not set.
+                </td>
+                {/stats.xms.set}
+              </tr>
+              <tr>
+                <td class="property-key">Max <code>mapred.job.map.memory.mb</code></td>
+                <td>{stats.jobMapMemoryMb.max}</td>
+                <td>{stats.jobMapMemoryMb.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max <code>mapred.job.reduce.memory.mb</code></td>
+                <td>{stats.jobReduceMemoryMb.max}</td>
+                <td>{stats.jobReduceMemoryMb.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max Distributed Cache</td>
+                {?stats.distributedCache.using}
+                <td>
+                  {stats.distributedCache.max}
+                </td>
+                <td>
+                  {stats.distributedCache.job}
+                </td>
+                {:else}
+                <td colspan="2">
+                  Not used.
+                </td>
+                {/stats.distributedCache.using}
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      <div class="row">
+        <div class="col-xs-12">
+          <h4>Counters</h4>
+          <table class="table table-bordered table-condensed">
+            <thead>
+              <tr>
+                <th class="property-key">Parameter</th>
+                <th class="property-key">Value</th>
+                <th>Job Name</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td class="property-key">Max <code>FILE_BYTES_READ</code></td>
+                <td>{stats.fileBytesRead.max}</td>
+                <td>{stats.fileBytesRead.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max <code>HDFS_BYTES_READ</code></td>
+                <td>{stats.hdfsBytesRead.max}</td>
+                <td>{stats.hdfsBytesRead.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max <code>FILE_BYTES_WRITTEN</code></td>
+                <td>{stats.fileBytesWritten.max}</td>
+                <td>{stats.fileBytesWritten.job}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Max <code>HDFS_BYTES_WRITTEN</code></td>
+                <td>{stats.hdfsBytesWritten.max}</td>
+                <td>{stats.hdfsBytesWritten.job}</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
diff --git a/src/tl/flowstats-no-data.tl b/src/tl/flowstats-no-data.tl
new file mode 100644
index 0000000..2f3605a
--- /dev/null
+++ b/src/tl/flowstats-no-data.tl
@@ -0,0 +1,8 @@
+      <div class="row">
+        <div class="col-xs-12">
+          <div class="alert alert-default">
+            <h4>No Flow Stats Available</h4>
+            <p>{message}</p>
+          </div>
+        </div>
+      </div>

src/tl/flowsummary.tl 160(+59 -101)

diff --git a/src/tl/flowsummary.tl b/src/tl/flowsummary.tl
index 10a0680..1c9154f 100644
--- a/src/tl/flowsummary.tl
+++ b/src/tl/flowsummary.tl
@@ -1,112 +1,70 @@
-        <div class="col-lg-12">
-          <table class="table table-bordered table-condensed table-striped">
+      <div class="row">
+        <div class="col-xs-12">
+          <table class="table table-bordered table-condensed">
             <tbody>
               <tr>
-                <td class="property-key">Flow name</td>
-                <td class="property-value-half">{flowName}</td>
                 <td class="property-key">Project name</td>
-                <td class="property-value-half">{projectName}</td>
+                <td>{projectName}</td>
               </tr>
               <tr>
-                <td class="property-key">Run As</td>
-                <td class="property-value-half">{user}</td>
                 <td class="property-key">Job Types Used</td>
-                <td class="property-value-half">{#jobTypes}{.} {/jobTypes}</td>
+                <td>{#jobTypes}{.} {/jobTypes}</td>
               </tr>
             </tbody>
           </table>
-					
-					<div class="panel panel-default">
-						<div class="panel-heading">
-							Scheduling
-							{?schedule}
-							<div class="pull-right">
-								<button type="button" id="removeSchedBtn" class="btn btn-xs btn-danger" onclick="removeSched({schedule.scheduleId})" >Remove Schedule</button>
-							</div>
-							{/schedule}
-						</div>
-						{?schedule}
-						<table class="table table-condensed table-bordered table-striped">
-							<tbody>
-								<tr>
-									<td class="property-key">Schedule ID</td>
-									<td class="property-value-half">{schedule.scheduleId}</td>
-									<td class="property-key">Submitted By</td>
-									<td class="property-value-half">{schedule.submitUser}</td>
-								</tr>
-								<tr>
-									<td class="property-key">First Scheduled to Run</td>
-									<td class="property-value-half">{schedule.firstSchedTime}</td>
-									<td class="property-key">Repeats Every</td>
-									<td class="property-value-half">{schedule.period}</td>
-								</tr>
-								<tr>
-									<td class="property-key">Next Execution Time</td>
-									<td class="property-value-half">{schedule.nextExecTime}</td>
-									<td class="property-key">SLA</td>
-									<td class="property-value-half">
-									{?schedule.slaOptions}
-										true 
-									{:else} 
-										false 
-									{/schedule.slaOptions}
-										<div class="pull-right">
-											<button type="button" id="addSlaBtn" class="btn btn-xs btn-primary" onclick="slaView.initFromSched({schedule.scheduleId}, '{flowName}')" >Set SLA</button>
-										</div>
-									</td>
-								</tr>
-							</tbody>
-						</table>
-						{:else}
-						<div class="panel-body">
-							<div class="alert alert-info">
-								<h4>No Schedule</h4>
-								<p>This flow has not been scheduled.</p>
-							</div>
-						</div>
-						{/schedule}
-					</div>
-
-          <div class="panel panel-default">
-            <div class="panel-heading">Last Run Stats</div>
-            {?lastRun}
-            <table class="table table-bordered table-condensed table-striped">
-              <tbody>
-								<tr>
-									<td class="property-key">Max Map Slots from Largest Job</td>
-									<td>{lastRun.maxMapSlots}</td>
-                </tr>
-                <tr>
-									<td class="property-key">Max Reduce Slots from Largest Job</td>
-									<td>{lastRun.maxReduceSlots}</td>
-								</tr>
-								<tr>
-									<td class="property-key">Total Map Slots from All Jobs</td>
-									<td>{lastRun.totalMapSlots}</td>
-                </tr>
-                <tr>
-									<td class="property-key">Total Reduce Slots from All Jobs</td>
-									<td>{lastRun.totalReduceSlots}</td>
-								</tr>
-								<tr>
-									<td class="property-key">Total Number of Jobs</td>
-									<td>{lastRun.numJobs}</td>
-                </tr>
-                <tr>
-									<td class="property-key">Longest Task Time</td>
-									<td>{lastRun.longestTaskTime}</td>
-								</tr>
-              </tbody>
-            </table>
-            {:else}
-            <div class="panel-body">
-              <div class="alert alert-info">
-                <h4>No last run stats available</h4>
-                <p>Last run stats requires at least one successful run of the flow.</p>
-              </div>
+        </div>
+      </div>
+      <hr>
+			
+      <div class="row">
+        <div class="col-xs-12">
+          <h3>
+            Scheduling
+            {?schedule}
+            <div class="pull-right">
+              <button type="button" id="removeSchedBtn" class="btn btn-sm btn-danger" onclick="removeSched({schedule.scheduleId})" >Remove Schedule</button>
             </div>
-            {/lastRun}
-          </div>
-        </div><!-- /.col-lg-12 -->
-
+            {/schedule}
+          </h3>
+          {?schedule}
+          <table class="table table-condensed table-bordered">
+            <tbody>
+              <tr>
+                <td class="property-key">Schedule ID</td>
+                <td class="property-value-half">{schedule.scheduleId}</td>
+                <td class="property-key">Submitted By</td>
+                <td class="property-value-half">{schedule.submitUser}</td>
+              </tr>
+              <tr>
+                <td class="property-key">First Scheduled to Run</td>
+                <td class="property-value-half">{schedule.firstSchedTime}</td>
+                <td class="property-key">Repeats Every</td>
+                <td class="property-value-half">{schedule.period}</td>
+              </tr>
+              <tr>
+                <td class="property-key">Next Execution Time</td>
+                <td class="property-value-half">{schedule.nextExecTime}</td>
+                <td class="property-key">SLA</td>
+                <td class="property-value-half">
+                {?schedule.slaOptions}
+                  true 
+                {:else} 
+                  false 
+                {/schedule.slaOptions}
+                  <div class="pull-right">
+                    <button type="button" id="addSlaBtn" class="btn btn-xs btn-primary" onclick="slaView.initFromSched({schedule.scheduleId}, '{flowName}')" >Set SLA</button>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+          {:else}
+            <div class="alert alert-default">
+              <h4>None</h4>
+              <p>This flow has not been scheduled.</p>
+            </div>
+          {/schedule}
 
+          <h3>Last Run Stats</h3>
+        </div>
+      </div>

src/tl/Makefile 17(+17 -0)

diff --git a/src/tl/Makefile b/src/tl/Makefile
new file mode 100644
index 0000000..a9c8028
--- /dev/null
+++ b/src/tl/Makefile
@@ -0,0 +1,17 @@
+OBJ_DIR = obj
+OBJ = \
+	$(OBJ_DIR)/flowsummary.js \
+	$(OBJ_DIR)/flowstats.js \
+	$(OBJ_DIR)/flowstats-no-data.js
+
+all: $(OBJ)
+
+$(OBJ_DIR)/%.js: %.tl
+	mkdir -p $(OBJ_DIR) && dustc --name=$(basename $<) $< $@
+
+clean:
+	rm -rf $(OBJ_DIR)
+
+.SUFFIXES: .tl .js
+
+.PHONY: all clean
diff --git a/src/web/js/azkaban/model/log-data.js b/src/web/js/azkaban/model/log-data.js
new file mode 100644
index 0000000..2e21386
--- /dev/null
+++ b/src/web/js/azkaban/model/log-data.js
@@ -0,0 +1,302 @@
+$.namespace('azkaban');
+
+azkaban.LogDataModel = Backbone.Model.extend({
+    TIMESTAMP_REGEX: /^.*? - /gm,
+
+    JOB_TRACKER_URL_REGEX: /https?:\/\/[-\w\.]+(?::\d+)?\/[\w\/\.]*\?\S+(job_\d{12}_\d{4,})\S*/,
+
+    // Command properties
+    COMMAND_START: "Command: ",
+    CLASSPATH_REGEX: /(?:-cp|-classpath)\s+(\S+)/g,
+    ENVIRONMENT_VARIABLES_REGEX: /-D(\S+)/g,
+    JVM_MEMORY_REGEX: /(-Xm\S+)/g,
+    PIG_PARAMS_REGEX: /-param\s+(\S+)/g,
+
+    JOB_TYPE_REGEX: /Building (\S+) job executor/,
+
+    PIG_JOB_SUMMARY_START: "HadoopVersion",
+    PIG_JOB_STATS_START: "Job Stats (time in seconds):",
+
+    HIVE_PARSING_START: "Parsing command: ",
+    HIVE_PARSING_END: "Parse Completed",
+    HIVE_NUM_MAP_REDUCE_JOBS_STRING: "Total MapReduce jobs = ",
+    HIVE_MAP_REDUCE_JOB_START: "Starting Job",
+    HIVE_MAP_REDUCE_JOBS_SUMMARY: "MapReduce Jobs Launched:",
+    HIVE_MAP_REDUCE_SUMMARY_REGEX: /Job (\d+): Map: (\d+)  Reduce: (\d+)   HDFS Read: (\d+) HDFS Write: (\d+)/,
+
+    initialize: function() {
+        this.set("offset", 0 );
+        this.set("logData", "");
+        this.on("change:logData", this.parseLogData);
+    },
+
+    refresh: function() {
+        var requestURL = contextURL + "/executor"; 
+        var finished = false;
+
+        var date = new Date();
+        var startTime = date.getTime();
+        
+        while (!finished) {
+            var requestData = {
+                "execid": execId,
+                "jobId": jobId,
+                "ajax":"fetchExecJobLogs",
+                "offset": this.get("offset"),
+                "length": 50000,
+                "attempt": attempt
+            };
+
+            var self = this;
+
+            var successHandler = function(data) {
+                console.log("fetchLogs");
+                if (data.error) {
+                    console.log(data.error);
+                    finished = true;
+                }
+                else if (data.length == 0) {
+                    finished = true;
+                }
+                else {
+                    var date = new Date();
+                    var endTime = date.getTime();
+                    if ((endTime - startTime) > 10000) {
+                        finished = true;
+                        showDialog("Alert","The log is taking a long time to finish loading. Azkaban has stopped loading them. Please click Refresh to restart the load.");
+                    }
+
+                    self.set("offset", data.offset + data.length);
+                    self.set("logData", self.get("logData") + data.data);
+                }
+            }
+
+            $.ajax({
+                url: requestURL,
+                type: "get",
+                async: false,
+                data: requestData,
+                dataType: "json",
+                error: function(data) {
+                    console.log(data);
+                    finished = true;
+                },
+                success: successHandler
+            });
+        }
+    },
+
+    parseLogData: function() {
+        var data = this.get("logData").replace(this.TIMESTAMP_REGEX, "");
+        var lines = data.split("\n");
+
+        if (this.parseCommand(lines)) {
+            this.parseJobTrackerUrls(lines);
+
+            var jobType = this.parseJobType(lines);
+            if (jobType.indexOf("pig") !== -1) {
+                this.parsePigTable(lines, "pigSummary", this.PIG_JOB_SUMMARY_START, "", 0);
+                this.parsePigTable(lines, "pigStats", this.PIG_JOB_STATS_START, "", 1);
+            } else if (jobType.indexOf("hive") !== -1) {
+                this.parseHiveQueries(lines);
+            }
+        }
+    },
+
+    parseCommand: function(lines) {
+        var commandStartIndex = -1;
+        var numLines = lines.length;
+        for (var i = 0; i < numLines; i++) {
+            if (lines[i].indexOf(this.COMMAND_START) === 0) {
+                commandStartIndex = i;
+                break;
+            }
+        }
+        
+        if (commandStartIndex != -1) {
+            var commandProperties = {};
+
+            var command = lines[commandStartIndex].substring(this.COMMAND_START.length);
+            commandProperties.Command = command;
+            
+            this.parseCommandProperty(command, commandProperties, "Classpath", this.CLASSPATH_REGEX, ':');
+            this.parseCommandProperty(command, commandProperties, "-D", this.ENVIRONMENT_VARIABLES_REGEX);
+            this.parseCommandProperty(command, commandProperties, "Memory Settings", this.JVM_MEMORY_REGEX);
+            this.parseCommandProperty(command, commandProperties, "Params", this.PIG_PARAMS_REGEX);
+            
+            this.set("commandProperties", commandProperties);
+
+            return true;
+        }
+        
+        return false;
+    },
+
+    parseCommandProperty: function(command, commandProperties, propertyName, regex, split) {
+        var results = [];
+        var match;
+        while (match = regex.exec(command)) {
+            if (split) {
+                results = results.concat(match[1].split(split));
+            } else {
+                results.push(match[1]);
+            }
+        }
+
+        if (results.length > 0) {
+            commandProperties[propertyName] = results;
+        }
+    },
+
+    parseJobTrackerUrls: function(lines) {
+        var jobTrackerUrls = {};
+        var jobTrackerUrlsOrdered = [];
+        var numLines = lines.length;
+        var match;
+        for (var i = 0; i < numLines; i++) {
+            if ((match = this.JOB_TRACKER_URL_REGEX.exec(lines[i])) && !jobTrackerUrls[match[1]]) {
+                jobTrackerUrls[match[1]] = match[0];
+                jobTrackerUrlsOrdered.push(match[0]);
+            }
+        }
+        this.set("jobTrackerUrls", jobTrackerUrls);
+        this.set("jobTrackerUrlsOrdered", jobTrackerUrlsOrdered);
+    },
+
+    parseJobType: function(lines) {
+        var numLines = lines.length;
+        var match;
+        for (var i = 0; numLines; i++) {
+            if (match = this.JOB_TYPE_REGEX.exec(lines[i])) {
+                return match[1];
+            }
+        }
+        
+        return null;
+    },
+
+    parsePigTable: function(lines, tableName, startPattern, endPattern, linesToSkipAfterStart) {
+        var index = -1;
+        var numLines = lines.length;
+        for (var i = 0; i < numLines; i++) {
+            if (lines[i].indexOf(startPattern) === 0) {
+                index = i + linesToSkipAfterStart;
+                break;
+            }
+        }
+        
+        if (index != -1) {
+            var table = [];
+            var line;
+            while ((line = lines[index]) !== endPattern) {
+                var columns = line.split("\t");
+                // If first column is a job id, make it a link to the job tracker.
+                if (this.get("jobTrackerUrls")[columns[0]]) {
+                    columns[0] = "<a href='" + this.get("jobTrackerUrls")[columns[0]] + "'>" + columns[0] + "</a>";
+                }
+                table.push(columns);
+                index++;
+            }
+
+            this.set(tableName, table);
+        }
+    },
+
+    parseHiveQueries: function(lines) {
+        var hiveQueries = [];
+        var hiveQueryJobs = [];
+
+        var currMapReduceJob = 0;
+        var numLines = lines.length;
+        for (var i = 0; i < numLines;) {
+            var line = lines[i];
+            var parsingCommandIndex = line.indexOf(this.HIVE_PARSING_START);
+            if (parsingCommandIndex === -1) {
+                i++;
+                continue;
+            }
+
+            // parse query text, which could span multiple lines
+            var queryStartIndex = parsingCommandIndex + this.HIVE_PARSING_START.length;
+            var query = line.substring(queryStartIndex) + "<br/>";
+            
+            i++;
+            while (i < numLines && (line = lines[i]).indexOf(this.HIVE_PARSING_END) === -1) {
+                query += line + "<br/>";
+                i++;
+            }
+            hiveQueries.push(query);
+            i++;
+            
+            // parse the query's Map-Reduce jobs, if any.
+            var numMRJobs = 0;
+            while (i < numLines) {
+                line = lines[i];
+                if (line.contains(this.HIVE_NUM_MAP_REDUCE_JOBS_STRING)) {
+                    // query involves map reduce jobs
+                    var numMRJobs = parseInt(line.substring(this.HIVE_NUM_MAP_REDUCE_JOBS_STRING.length),10);
+                    i++;
+                    
+                    // get the map reduce jobs summary
+                    while (i < numLines) {
+                        line = lines[i];
+                        if (line.contains(this.HIVE_MAP_REDUCE_JOBS_SUMMARY)) {
+                            // job summary table found
+                            i++;
+                            
+                            var queryJobs = [];
+                            
+                            var previousJob = -1;
+                            var numJobsSeen = 0;
+                            while (numJobsSeen < numMRJobs && i < numLines) {
+                                line = lines[i];
+                                var match;
+                                if (match = this.HIVE_MAP_REDUCE_SUMMARY_REGEX.exec(line)) {
+                                    var currJob = parseInt(match[1], 10);
+                                    if (currJob === previousJob) {
+                                        i++;
+                                        continue;
+                                    }
+                                    
+                                    var job = [];
+                                    job.push("<a href='" + this.get("jobTrackerUrlsOrdered")[currMapReduceJob++] + "'>" + currJob + "</a>");
+                                    job.push(match[2]);
+                                    job.push(match[3]);
+                                    job.push(match[4]);
+                                    job.push(match[5]);
+                                    queryJobs.push(job);
+                                    previousJob = currJob;
+                                    numJobsSeen++;
+                                }
+                                i++;
+                            }
+                            
+                            if (numJobsSeen === numMRJobs) {
+                                hiveQueryJobs.push(queryJobs);
+                            }
+                            
+                            break;
+                        }
+                        i++;
+                    } 
+                    break;
+                }
+                else if (line.contains(this.HIVE_PARSING_START)) {
+                    if (numMRJobs === 0) {
+                        hiveQueryJobs.push(null);
+                    }
+                    break;
+                }
+                i++;
+            }
+            continue;
+        }
+
+        if (hiveQueries.length > 0) {
+            this.set("hiveSummary", {
+                hiveQueries: hiveQueries,
+                hiveQueryJobs: hiveQueryJobs
+            });
+        }
+    }
+});
diff --git a/src/web/js/azkaban/view/flow-stats.js b/src/web/js/azkaban/view/flow-stats.js
new file mode 100644
index 0000000..5f2919e
--- /dev/null
+++ b/src/web/js/azkaban/view/flow-stats.js
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2012 LinkedIn Corp.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+$.namespace('azkaban');
+
+azkaban.FlowStatsModel = Backbone.Model.extend({});
+azkaban.FlowStatsView = Backbone.View.extend({
+  events: {
+  },
+
+	initialize: function(settings) {
+		this.model.bind('change:view', this.handleChangeView, this);
+		this.model.bind('render', this.render, this);
+  },
+	
+  render: function(evt) {
+  },
+
+  show: function(execId) {
+    this.analyzeExecution(execId);
+  },
+
+  fetchJobs: function(execId) {
+    var requestURL = contextURL + "/executor";
+    var requestData = {"execid": execId, "ajax":"fetchexecflow"};
+    var jobs = [];
+    var successHandler = function(data) {
+      for (var i = 0; i < data.nodes.length; ++i) {
+        var node = data.nodes[i];
+        jobs.push(node.id);
+      }
+    };
+    $.ajax({
+      url: requestURL,
+      data: requestData,
+      success: successHandler,
+      dataType: "json",
+      async: false
+    });
+    return jobs;
+  },
+
+  fetchJobStats: function(jobId, execId) {
+    var requestURL = contextURL + "/executor";
+    var requestData = {
+      "execid": execId,
+      "flowid": flowId,
+      "jobid": jobId,
+      "ajax": "fetchExecJobStats"
+    };
+    var stats = null;
+    var successHandler = function(data) {
+      stats = data;
+    };
+    $.ajax({
+      url: requestURL,
+      data: requestData,
+      success: successHandler,
+      dataType: "json",
+      async: false
+    });
+    return stats;
+  },
+
+  updateStatsMapred: function(state, data, job) {
+    var stats = data.stats;
+    var mappers = parseInt(state.totalMappers);
+    var reducers = parseInt(state.totalReducers);
+    if (mappers >= stats.mapSlots.max) {
+      stats.mapSlots.max = mappers;
+      stats.mapSlots.job = job;
+    }
+    if (reducers >= stats.reduceSlots.max) {
+      stats.reduceSlots.max = reducers;
+      stats.reduceSlots.job = job;
+    }
+    stats.totalMapSlots += mappers;
+    stats.totalReduceSlots += reducers;
+
+  },
+
+  updateStatsConf: function(conf, data, job) {
+    var stats = data.stats;
+    if (conf == null) {
+      data.warnings.push("No job conf available for job " + job);
+      return;
+    }
+
+    var jobMapMemoryMb = parseInt(conf['mapred.job.map.memory.mb']);
+    if (jobMapMemoryMb >= stats.jobMapMemoryMb.max) {
+      stats.jobMapMemoryMb.max = jobMapMemoryMb;
+      stats.jobMapMemoryMb.job = job;
+    }
+    var jobReduceMemoryMb = parseInt(conf['mapred.job.reduce.memory.mb']);
+    if (jobReduceMemoryMb >= stats.jobReduceMemoryMb.max) {
+      stats.jobReduceMemoryMb.max = jobReduceMemoryMb;
+      stats.jobReduceMemoryMb.job = job;
+    }
+
+    var childJavaOpts = conf['mapred.child.java.opts'];
+    var parts = childJavaOpts.split(" ");
+    for (var i = 0; i < parts.length; ++i) {
+      var str = parts[i];
+      if (str.indexOf('Xmx') > -1) {
+        if (str.length <= 4) {
+          continue;
+        }
+        var size = str.substring(4, str.length);
+        var val = sizeStrToBytes(size);
+        if (val >= stats.xmx.max) {
+          stats.xmx.max = val;
+          stats.xmx.str = size;
+          stats.xmx.job = job;
+        }
+      }
+      if (str.indexOf('Xms') > -1) {
+        if (str.length <= 4) { 
+          continue;
+        }
+        var size = str.substring(4, str.length);
+        var val = sizeStrToBytes(size);
+        stats.xms.set = true;
+        if (val >= stats.xms.max) {
+          stats.xms.max = val;
+          stats.xms.str = size;
+          stats.xms.job = job;
+        }
+      }
+    }
+
+    var cacheFiles = conf['mapred.cache.files'];
+    var cacheFilesFilesizes = conf['mapred.cache.files.filesizes'];
+    if (cacheFiles != null && cacheFilesFilesizes != null) {
+      stats.distributedCache.using = true;
+      var parts = cacheFilesFilesizes.split(',');
+      var size = 0;
+      for (var i = 0; i < parts.length; ++i) {
+        size += parseInt(parts[i]);
+      }
+      if (size >= stats.distributedCache.max) {
+        stats.distributedCache.max = size;
+        stats.distributedCache.job = job;
+      }
+    }
+  },
+
+  updateStatsCounters: function(state, data, job) {
+    var stats = data.stats;
+    if (state.counters == null) {
+      data.warnings.push("No job counters available for job " + job);
+      return;
+    }
+    var fileSystemCounters = state.counters['FileSystemCounters'];
+    if (fileSystemCounters == null) {
+      data.warnings.push("No FileSystemCounters available for job " + job);
+      return;
+    }
+    var fileBytesRead = parseInt(fileSystemCounters['FILE_BYTES_READ']);
+    if (fileBytesRead >= stats.fileBytesRead.max) {
+      stats.fileBytesRead.max = fileBytesRead;
+      stats.fileBytesRead.job = job;
+    }
+
+    var fileBytesWritten = parseInt(fileSystemCounters['FILE_BYTES_WRITTEN']);
+    if (fileBytesWritten >= stats.fileBytesWritten.max) {
+      stats.fileBytesWritten.max = fileBytesWritten;
+      stats.fileBytesWritten.job = job;
+    }
+    
+    var hdfsBytesRead = parseInt(fileSystemCounters['HDFS_BYTES_READ']);
+    if (hdfsBytesRead >= stats.hdfsBytesRead.max) {
+      stats.hdfsBytesRead.max = hdfsBytesRead;
+      stats.hdfsBytesRead.job = job;
+    }
+    
+    var hdfsBytesWritten = parseInt(fileSystemCounters['HDFS_BYTES_WRITTEN']);
+    if (hdfsBytesWritten >= stats.hdfsBytesWritten.max) {
+      stats.hdfsBytesWritten.max = hdfsBytesWritten;
+      stats.hdfsBytesWritten.job = job;
+    }
+  },
+
+  updateStats: function(jobStats, data, job) {
+    var stats = data.stats;
+    var state = jobStats.state;
+    var conf = jobStats.conf;
+
+    this.updateStatsMapred(state, data, job);
+    this.updateStatsConf(conf, data, job);
+    this.updateStatsCounters(state, data, job);
+  },
+
+  finalizeStats: function(data) {
+    data.success = true;
+  },
+
+  analyzeExecution: function(execId) {
+    var jobs = this.fetchJobs(execId);
+    if (jobs == null) {
+      this.model.set({'data': null});
+      this.model.trigger('render');
+      return;
+    }
+
+    var data = {
+      success: false,
+      message: null,
+      warnings: [],
+      stats: {
+        mapSlots: {
+          max: 0,
+          job: null
+        },
+        reduceSlots: {
+          max: 0,
+          job: null
+        },
+        totalMapSlots: 0,
+        totalReduceSlots: 0,
+        numJobs: jobs.length,
+        longestTaskTime: 0,
+        jobMapMemoryMb: {
+          max: 0,
+          job: null
+        },
+        jobReduceMemoryMb: {
+          max: 0,
+          job: null
+        },
+        xmx: {
+          max: 0,
+          str: null,
+          job: null
+        },
+        xms: {
+          set: false,
+          max: 0,
+          str: null,
+          job: null
+        },
+        fileBytesRead: {
+          max: 0,
+          job: null
+        },
+        hdfsBytesRead: {
+          max: 0,
+          job: null
+        },
+        fileBytesWritten: {
+          max: 0,
+          job: null
+        },
+        hdfsBytesWritten: {
+          max: 0,
+          job: null
+        },
+        distributedCache: {
+          using: false,
+          max: 0,
+          job: null
+        },
+      }
+    };
+
+    for (var i = 0; i < jobs.length; ++i) {
+      var job = jobs[i];
+      var jobStats = this.fetchJobStats(job, execId);
+      if (jobStats.jobStats == null) {
+        data.warnings.push("No job stats available for job " + job.id);
+        continue;
+      }
+      for (var j = 0; j < jobStats.jobStats.length; ++j) {
+        this.updateStats(jobStats.jobStats[j], data, job);
+      }
+    }
+    this.finalizeStats(data);
+    this.model.set({'data': data});
+    this.model.trigger('render');
+  },
+
+	render: function(evt) {
+    var view = this;
+    var data = this.model.get('data');
+    if (data == null) {
+      var msg = { message: "Error retrieving flow stats."};
+      dust.render("flowstats-no-data", msg, function(err, out) {
+        view.display(out);
+      });
+    }
+    else if (data.success == "false") {
+      dust.render("flowstats-no-data", data, function(err, out) {
+        view.display(out);
+      });
+    }
+    else {
+      dust.render("flowstats", data, function(err, out) {
+        view.display(out);
+      });
+    }
+  },
+
+  display: function(out) {
+    $('#flow-stats-container').html(out);
+  },
+});
diff --git a/src/web/js/azkaban/view/job-details.js b/src/web/js/azkaban/view/job-details.js
new file mode 100644
index 0000000..f191f37
--- /dev/null
+++ b/src/web/js/azkaban/view/job-details.js
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2012 LinkedIn Corp.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+$.namespace('azkaban');
+
+var jobLogView;
+azkaban.JobLogView = Backbone.View.extend({
+	events: {
+		"click #updateLogBtn" : "refresh"
+	},
+
+	initialize: function() {
+		this.listenTo(this.model, "change:logData", this.render);
+	},
+
+	refresh: function() {
+		this.model.refresh();
+	},
+
+	render: function() {
+		var re = /(https?:\/\/(([-\w\.]+)+(:\d+)?(\/([\w/_\.]*(\?\S+)?)?)?))/g;
+		var log = this.model.get("logData");
+		log = log.replace(re, "<a href=\"$1\" title=\"\">$1</a>");
+		$("#logSection").html(log);
+	}
+});
+
+var jobSummaryView;
+azkaban.JobSummaryView = Backbone.View.extend({
+	events: {
+		"click #updateSummaryBtn" : "refresh"
+	},
+
+	initialize: function(settings) {
+		$("#commandSummary").hide();
+		$("#pigJobSummary").hide();
+		$("#pigJobStats").hide();
+		$("#hiveJobSummary").hide();
+
+		this.listenTo(this.model, "change:commandProperties", this.renderCommandTable);
+		this.listenTo(this.model, "change:pigSummary", this.renderPigSummaryTable);
+		this.listenTo(this.model, "change:pigStats", this.renderPigStatsTable);
+		this.listenTo(this.model, "change:hiveSummary", this.renderHiveTable);
+	},
+
+	refresh: function() {
+		this.model.refresh();
+	},
+
+	handleUpdate: function(evt) {
+		renderJobTable(jobSummary.summaryTableHeaders, jobSummary.summaryTableData, "summary");
+		renderJobTable(jobSummary.statTableHeaders, jobSummary.statTableData, "stats");
+		renderHiveTable(jobSummary.hiveQueries, jobSummary.hiveQueryJobs);
+	},
+	renderCommandTable: function() {
+		var commandTable = $("#commandTable");
+		var commandProperties = this.model.get("commandProperties");
+
+		for (var key in commandProperties) {
+			if (commandProperties.hasOwnProperty(key)) {
+				var value = commandProperties[key];
+				if (Array.isArray(value)) {
+					value = value.join("<br/>");
+				}
+				var tr = document.createElement("tr");
+				var keyTd = document.createElement("td");
+				var valueTd = document.createElement("td");
+				$(keyTd).html("<b>" + key + "</b>");
+				$(valueTd).html(value);
+				$(tr).append(keyTd);
+				$(tr).append(valueTd);
+				commandTable.append(tr);
+			}
+		}
+
+		$("#commandSummary").show();
+	},
+	renderPigTable: function(tableName, data) {
+		// Add table headers
+		var header = $("#" + tableName + "Header");
+		var tr = document.createElement("tr");
+		var i;
+		var headers = data[0];
+		var numColumns = headers.length;
+		for (i = 0; i < numColumns; i++) {
+			var th = document.createElement("th");
+			$(th).text(headers[i]);
+			$(tr).append(th);
+		}
+		header.append(tr);
+		
+		// Add table body
+		var body = $("#" + tableName + "Body");
+		for (i = 1; i < data.length; i++) {
+			tr = document.createElement("tr");
+			var row = data[i];
+			for (var j = 0; j < numColumns; j++) {
+				var td = document.createElement("td");
+				if (j == 0) {
+					// first column is a link to job details page 
+					$(td).html(row[j]);
+				} else {
+					$(td).text(row[j]);
+				}
+				$(tr).append(td);
+			}
+			body.append(tr);
+		}
+
+		$("#pigJob" + tableName.charAt(0).toUpperCase() + tableName.substring(1)).show();
+	},
+	renderPigSummaryTable: function() {
+		this.renderPigTable("summary", this.model.get("pigSummary"));
+	},
+	renderPigStatsTable: function() {
+		this.renderPigTable("stats", this.model.get("pigStats"));
+	},
+	renderHiveTable: function() {
+		var hiveSummary = this.model.get("hiveSummary");
+		var queries = hiveSummary.hiveQueries;
+		var queryJobs = hiveSummary.hiveQueryJobs;
+
+		// Set up table column headers
+		var header = $("#hiveTableHeader");
+		var tr = document.createElement("tr");
+		var headers = ["Query","Job","Map","Reduce","HDFS Read","HDFS Write"];
+		var i;
+		
+		for (i = 0; i < headers.length; i++) {
+			var th = document.createElement("th");
+			$(th).text(headers[i]);
+			$(tr).append(th);
+		}
+		header.html(tr);
+		
+		// Construct table body
+		var oldBody = $("#hiveTableBody");
+		var newBody = $(document.createElement("tbody")).attr("id", "hiveTableBody");
+		for (i = 0; i < queries.length; i++) {
+			// new query
+			tr = document.createElement("tr");
+			var td = document.createElement("td");
+			$(td).html("<b>" + queries[i] + "</b>");
+			$(tr).append(td);
+			
+			var jobs = queryJobs[i];
+			if (jobs != null) {
+				// add first job for this query
+				var jobValues = jobs[0];
+				var j;
+				for (j = 0; j < jobValues.length; j++) {
+					td = document.createElement("td");
+					$(td).html(jobValues[j]);
+					$(tr).append(td);
+				}
+				newBody.append(tr);
+				
+				// add remaining jobs for this query
+				for (j = 1; j < jobs.length; j++) {
+					jobValues = jobs[j];
+					tr = document.createElement("tr");
+					
+					// add empty cell for query column
+					td = document.createElement("td");
+					$(td).html("&nbsp;");
+					$(tr).append(td);
+					
+					// add job values
+					for (var k = 0; k < jobValues.length; k++) {
+						td = document.createElement("td");
+						$(td).html(jobValues[k]);
+						$(tr).append(td);
+					}
+					newBody.append(tr);
+				}
+				
+			} else {
+				newBody.append(tr);
+			}
+		}
+		oldBody.replaceWith(newBody);
+
+		$("#hiveJobSummary").show();
+	}
+});
+
+var jobTabView;
+azkaban.JobTabView = Backbone.View.extend({
+	events: {
+		'click #jobSummaryViewLink': 'handleJobSummaryViewLinkClick',
+		'click #jobLogViewLink': 'handleJobLogViewLinkClick'
+	},
+
+	initialize: function(settings) {
+		var selectedView = settings.selectedView;
+		if (selectedView == 'joblog') {
+			this.handleJobLogViewLinkClick();
+		}
+		else {
+			this.handleJobSummaryViewLinkClick();
+		}
+	},
+
+	handleJobLogViewLinkClick: function() {
+		$('#jobSummaryViewLink').removeClass('active');
+		$('#jobSummaryView').hide();
+		$('#jobLogViewLink').addClass('active');
+		$('#jobLogView').show();
+	},
+	
+	handleJobSummaryViewLinkClick: function() {
+		$('#jobSummaryViewLink').addClass('active');
+		$('#jobSummaryView').show();
+		$('#jobLogViewLink').removeClass('active');
+		$('#jobLogView').hide();
+	},
+});
+
+var showDialog = function(title, message) {
+  $('#messageTitle').text(title);
+  $('#messageBox').text(message);
+  $('#messageDialog').modal({
+		closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+		position: ["20%",],
+		containerId: 'confirm-container',
+		containerCss: {
+			'height': '220px',
+			'width': '565px'
+		},
+		onShow: function (dialog) {
+		}
+	});
+}
+
+$(function() {
+	var logDataModel = new azkaban.LogDataModel();
+	
+	jobLogView = new azkaban.JobLogView({
+		el: $('#jobLogView'), 
+		model: logDataModel
+	});
+
+	jobSummaryView = new azkaban.JobSummaryView({
+		el: $('#jobSummaryView'), 
+		model: logDataModel
+	});
+
+	jobTabView = new azkaban.JobTabView({
+		el: $('#headertabs')
+	});
+
+	logDataModel.refresh();
+
+	if (window.location.hash) {
+		var hash = window.location.hash;
+		if (hash == '#joblog') {
+			jobTabView.handleJobLogViewLinkClick();
+		}
+		else if (hash == '#jobsummary') {
+			jobTabView.handleJobSummaryViewLinkClick();
+		}
+	}
+});
diff --git a/src/web/js/azkaban/view/job-history.js b/src/web/js/azkaban/view/job-history.js
new file mode 100644
index 0000000..a205d22
--- /dev/null
+++ b/src/web/js/azkaban/view/job-history.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012 LinkedIn Corp.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+$.namespace('azkaban');
+
+var jobHistoryView;
+
+var dataModel;
+azkaban.DataModel = Backbone.Model.extend({});
+
+$(function() {
+	var selected;
+	var series = dataSeries;
+	dataModel = new azkaban.DataModel();
+	dataModel.set({
+		"data": series
+	});
+  dataModel.trigger('render');
+
+	jobHistoryView = new azkaban.TimeGraphView({
+		el: $('#timeGraph'), 
+		model: dataModel,
+    modelField: "data"
+	});
+});
diff --git a/src/web/js/dust-full-2.2.3.min.js b/src/web/js/dust-full-2.2.3.min.js
new file mode 100644
index 0000000..8ba0201
--- /dev/null
+++ b/src/web/js/dust-full-2.2.3.min.js
@@ -0,0 +1,5 @@
+/*! Dust - Asynchronous Templating - v2.2.3
+* http://linkedin.github.io/dustjs/
+* Copyright (c) 2013 Aleksander Williams; Released under the MIT License */
+function getGlobal(){return function(){return this.dust}.call(null)}var dust={};!function(dust){function Context(a,b,c,d){this.stack=a,this.global=b,this.blocks=c,this.templateName=d}function Stack(a,b,c,d){this.tail=b,this.isObject=a&&"object"==typeof a,this.head=a,this.index=c,this.of=d}function Stub(a){this.head=new Chunk(this),this.callback=a,this.out=""}function Stream(){this.head=new Chunk(this)}function Chunk(a,b,c){this.root=a,this.next=b,this.data=[],this.flushable=!1,this.taps=c}function Tap(a,b){this.head=a,this.tail=b}if(dust){var ERROR="ERROR",WARN="WARN",INFO="INFO",DEBUG="DEBUG",levels=[DEBUG,INFO,WARN,ERROR],EMPTY_FUNC=function(){},logger=EMPTY_FUNC;dust.isDebug=!1,dust.debugLevel=INFO,"undefined"!=typeof window&&window&&window.console&&window.console.log?logger=window.console.log:"undefined"!=typeof console&&console&&console.log&&(logger=console.log),dust.log=function(a,b){b=b||INFO,dust.isDebug&&levels.indexOf(b)>=levels.indexOf(dust.debugLevel)&&(dust.logQueue||(dust.logQueue=[]),dust.logQueue.push({message:a,type:b}),logger.call(console||window.console,"[DUST "+b+"]: "+a))},dust.onError=function(a,b){if(dust.log(a.message||a,ERROR),dust.isDebug)throw a;return b},dust.helpers={},dust.cache={},dust.register=function(a,b){a&&(dust.cache[a]=b)},dust.render=function(a,b,c){var d=new Stub(c).head;try{dust.load(a,d,Context.wrap(b,a)).end()}catch(e){dust.onError(e,d)}},dust.stream=function(a,b){var c=new Stream;return dust.nextTick(function(){try{dust.load(a,c.head,Context.wrap(b,a)).end()}catch(d){dust.onError(d,c.head)}}),c},dust.renderSource=function(a,b,c){return dust.compileFn(a)(b,c)},dust.compileFn=function(a,b){var c=dust.loadSource(dust.compile(a,b));return function(a,d){var e=d?new Stub(d):new Stream;return dust.nextTick(function(){"function"==typeof c?c(e.head,Context.wrap(a,b)).end():dust.onError(new Error("Template ["+b+"] cannot be resolved to a Dust function"))}),e}},dust.load=function(a,b,c){var d=dust.cache[a];return d?d(b,c):dust.onLoad?b.map(function(b){dust.onLoad(a,function(d,e){return d?b.setError(d):(dust.cache[a]||dust.loadSource(dust.compile(e,a)),dust.cache[a](b,c).end(),void 0)})}):b.setError(new Error("Template Not Found: "+a))},dust.loadSource=function(source,path){return eval(source)},dust.isArray=Array.isArray?Array.isArray:function(a){return"[object Array]"===Object.prototype.toString.call(a)},dust.nextTick=function(){return"undefined"!=typeof process?process.nextTick:function(a){setTimeout(a,0)}}(),dust.isEmpty=function(a){return dust.isArray(a)&&!a.length?!0:0===a?!1:!a},dust.filter=function(a,b,c){if(c)for(var d=0,e=c.length;e>d;d++){var f=c[d];"s"===f?(b=null,dust.log("Using unescape filter on ["+a+"]",DEBUG)):"function"==typeof dust.filters[f]?a=dust.filters[f](a):dust.onError(new Error("Invalid filter ["+f+"]"))}return b&&(a=dust.filters[b](a)),a},dust.filters={h:function(a){return dust.escapeHtml(a)},j:function(a){return dust.escapeJs(a)},u:encodeURI,uc:encodeURIComponent,js:function(a){return JSON?JSON.stringify(a):(dust.log("JSON is undefined.  JSON stringify has not been used on ["+a+"]",WARN),a)},jp:function(a){return JSON?JSON.parse(a):(dust.log("JSON is undefined.  JSON parse has not been used on ["+a+"]",WARN),a)}},dust.makeBase=function(a){return new Context(new Stack,a)},Context.wrap=function(a,b){return a instanceof Context?a:new Context(new Stack(a),{},null,b)},Context.prototype.get=function(a,b){return"string"==typeof a&&("."===a[0]&&(b=!0,a=a.substr(1)),a=a.split(".")),this._get(b,a)},Context.prototype._get=function(a,b){var c,d,e,f,g=this.stack,h=1;if(dust.log("Searching for reference [{"+b.join(".")+"}] in template ["+this.getTemplateName()+"]",DEBUG),d=b[0],e=b.length,a&&0===e)f=g,g=g.head;else{if(a)g=g.head[d];else{for(;g&&(!g.isObject||(f=g.head,c=g.head[d],void 0===c));)g=g.tail;g=void 0!==c?c:this.global?this.global[d]:void 0}for(;g&&e>h;)f=g,g=g[b[h]],h++}if("function"==typeof g){var i=function(){return g.apply(f,arguments)};return i.isFunction=!0,i}return void 0===g&&dust.log("Cannot find the value for reference [{"+b.join(".")+"}] in template ["+this.getTemplateName()+"]"),g},Context.prototype.getPath=function(a,b){return this._get(a,b)},Context.prototype.push=function(a,b,c){return new Context(new Stack(a,this.stack,b,c),this.global,this.blocks,this.getTemplateName())},Context.prototype.rebase=function(a){return new Context(new Stack(a),this.global,this.blocks,this.getTemplateName())},Context.prototype.current=function(){return this.stack.head},Context.prototype.getBlock=function(a){if("function"==typeof a){var b=new Chunk;a=a(b,this).data.join("")}var c=this.blocks;if(!c)return dust.log("No blocks for context[{"+a+"}] in template ["+this.getTemplateName()+"]",DEBUG),void 0;for(var d,e=c.length;e--;)if(d=c[e][a])return d},Context.prototype.shiftBlocks=function(a){var b,c=this.blocks;return a?(b=c?c.concat([a]):[a],new Context(this.stack,this.global,b,this.getTemplateName())):this},Context.prototype.getTemplateName=function(){return this.templateName},Stub.prototype.flush=function(){for(var a=this.head;a;){if(!a.flushable)return a.error?(this.callback(a.error),dust.onError(new Error("Chunk error ["+a.error+"] thrown. Ceasing to render this template.")),this.flush=EMPTY_FUNC,void 0):void 0;this.out+=a.data.join(""),a=a.next,this.head=a}this.callback(null,this.out)},Stream.prototype.flush=function(){for(var a=this.head;a;){if(!a.flushable)return a.error?(this.emit("error",a.error),dust.onError(new Error("Chunk error ["+a.error+"] thrown. Ceasing to render this template.")),this.flush=EMPTY_FUNC,void 0):void 0;this.emit("data",a.data.join("")),a=a.next,this.head=a}this.emit("end")},Stream.prototype.emit=function(a,b){if(!this.events)return dust.log("No events to emit",INFO),!1;var c=this.events[a];if(!c)return dust.log("Event type ["+a+"] does not exist",WARN),!1;if("function"==typeof c)c(b);else if(dust.isArray(c))for(var d=c.slice(0),e=0,f=d.length;f>e;e++)d[e](b);else dust.onError(new Error("Event Handler ["+c+"] is not of a type that is handled by emit"))},Stream.prototype.on=function(a,b){return this.events||(this.events={}),this.events[a]?"function"==typeof this.events[a]?this.events[a]=[this.events[a],b]:this.events[a].push(b):(dust.log("Event type ["+a+"] does not exist. Using just the specified callback.",WARN),b?this.events[a]=b:dust.log("Callback for type ["+a+"] does not exist. Listener not registered.",WARN)),this},Stream.prototype.pipe=function(a){return this.on("data",function(b){try{a.write(b,"utf8")}catch(c){dust.onError(c,a.head)}}).on("end",function(){try{return a.end()}catch(b){dust.onError(b,a.head)}}).on("error",function(b){a.error(b)}),this},Chunk.prototype.write=function(a){var b=this.taps;return b&&(a=b.go(a)),this.data.push(a),this},Chunk.prototype.end=function(a){return a&&this.write(a),this.flushable=!0,this.root.flush(),this},Chunk.prototype.map=function(a){var b=new Chunk(this.root,this.next,this.taps),c=new Chunk(this.root,b,this.taps);return this.next=c,this.flushable=!0,a(c),b},Chunk.prototype.tap=function(a){var b=this.taps;return this.taps=b?b.push(a):new Tap(a),this},Chunk.prototype.untap=function(){return this.taps=this.taps.tail,this},Chunk.prototype.render=function(a,b){return a(this,b)},Chunk.prototype.reference=function(a,b,c,d){return"function"==typeof a&&(a.isFunction=!0,a=a.apply(b.current(),[this,b,null,{auto:c,filters:d}]),a instanceof Chunk)?a:dust.isEmpty(a)?this:this.write(dust.filter(a,c,d))},Chunk.prototype.section=function(a,b,c,d){if("function"==typeof a&&(a=a.apply(b.current(),[this,b,c,d]),a instanceof Chunk))return a;var e=c.block,f=c["else"];if(d&&(b=b.push(d)),dust.isArray(a)){if(e){var g=a.length,h=this;if(g>0){b.stack.head&&(b.stack.head.$len=g);for(var i=0;g>i;i++)b.stack.head&&(b.stack.head.$idx=i),h=e(h,b.push(a[i],i,g));return b.stack.head&&(b.stack.head.$idx=void 0,b.stack.head.$len=void 0),h}if(f)return f(this,b)}}else if(a===!0){if(e)return e(this,b)}else if(a||0===a){if(e)return e(this,b.push(a))}else if(f)return f(this,b);return dust.log("Not rendering section (#) block in template ["+b.getTemplateName()+"], because above key was not found",DEBUG),this},Chunk.prototype.exists=function(a,b,c){var d=c.block,e=c["else"];if(dust.isEmpty(a)){if(e)return e(this,b)}else if(d)return d(this,b);return dust.log("Not rendering exists (?) block in template ["+b.getTemplateName()+"], because above key was not found",DEBUG),this},Chunk.prototype.notexists=function(a,b,c){var d=c.block,e=c["else"];if(dust.isEmpty(a)){if(d)return d(this,b)}else if(e)return e(this,b);return dust.log("Not rendering not exists (^) block check in template ["+b.getTemplateName()+"], because above key was found",DEBUG),this},Chunk.prototype.block=function(a,b,c){var d=c.block;return a&&(d=a),d?d(this,b):this},Chunk.prototype.partial=function(a,b,c){var d;d=dust.makeBase(b.global),d.blocks=b.blocks,b.stack&&b.stack.tail&&(d.stack=b.stack.tail),c&&(d=d.push(c)),"string"==typeof a&&(d.templateName=a),d=d.push(b.stack.head);var e;return e="function"==typeof a?this.capture(a,d,function(a,b){d.templateName=d.templateName||a,dust.load(a,b,d).end()}):dust.load(a,this,d)},Chunk.prototype.helper=function(a,b,c,d){var e=this;try{return dust.helpers[a]?dust.helpers[a](e,b,c,d):dust.onError(new Error("Invalid helper ["+a+"]"),e)}catch(f){return dust.onError(f,e)}},Chunk.prototype.capture=function(a,b,c){return this.map(function(d){var e=new Stub(function(a,b){a?d.setError(a):c(b,d)});a(e.head,b).end()})},Chunk.prototype.setError=function(a){return this.error=a,this.root.flush(),this},Tap.prototype.push=function(a){return new Tap(a,this)},Tap.prototype.go=function(a){for(var b=this;b;)a=b.head(a),b=b.tail;return a};var HCHARS=new RegExp(/[&<>\"\']/),AMP=/&/g,LT=/</g,GT=/>/g,QUOT=/\"/g,SQUOT=/\'/g;dust.escapeHtml=function(a){return"string"==typeof a?HCHARS.test(a)?a.replace(AMP,"&amp;").replace(LT,"&lt;").replace(GT,"&gt;").replace(QUOT,"&quot;").replace(SQUOT,"&#39;"):a:a};var BS=/\\/g,FS=/\//g,CR=/\r/g,LS=/\u2028/g,PS=/\u2029/g,NL=/\n/g,LF=/\f/g,SQ=/'/g,DQ=/"/g,TB=/\t/g;dust.escapeJs=function(a){return"string"==typeof a?a.replace(BS,"\\\\").replace(FS,"\\/").replace(DQ,'\\"').replace(SQ,"\\'").replace(CR,"\\r").replace(LS,"\\u2028").replace(PS,"\\u2029").replace(NL,"\\n").replace(LF,"\\f").replace(TB,"\\t"):a}}}(dust),"undefined"!=typeof exports&&("undefined"!=typeof process&&require("./server")(dust),module.exports=dust);var dustCompiler=function(dust){function a(a){var b={};return dust.filterNode(b,a)}function b(a,b){var c,d,e,f=[b[0]];for(c=1,d=b.length;d>c;c++)e=dust.filterNode(a,b[c]),e&&f.push(e);return f}function c(a,b){var c,d,e,f,g=[b[0]];for(d=1,e=b.length;e>d;d++)f=dust.filterNode(a,b[d]),f&&("buffer"===f[0]?c?c[1]+=f[1]:(c=f,g.push(f)):(c=null,g.push(f)));return g}function d(a,b){return["buffer",l[b[1]]]}function e(a,b){return b}function f(){}function g(a,b){var c={name:b,bodies:[],blocks:{},index:0,auto:"h"};return"(function(){dust.register("+(b?'"'+b+'"':"null")+","+dust.compileNode(c,a)+");"+h(c)+i(c)+"return body_0;})();"}function h(a){var b,c=[],d=a.blocks;for(b in d)c.push('"'+b+'":'+d[b]);return c.length?(a.blocks="ctx=ctx.shiftBlocks(blocks);","var blocks={"+c.join(",")+"};"):a.blocks=""}function i(a){var b,c,d=[],e=a.bodies,f=a.blocks;for(b=0,c=e.length;c>b;b++)d[b]="function body_"+b+"(chk,ctx){"+f+"return chk"+e[b]+";}";return d.join("")}function j(a,b){var c,d,e="";for(c=1,d=b.length;d>c;c++)e+=dust.compileNode(a,b[c]);return e}function k(a,b,c){return"."+c+"("+dust.compileNode(a,b[1])+","+dust.compileNode(a,b[2])+","+dust.compileNode(a,b[4])+","+dust.compileNode(a,b[3])+")"}dust.compile=function(b,c){try{var d=a(dust.parse(b));return g(d,c)}catch(e){if(!e.line||!e.column)throw e;throw new SyntaxError(e.message+" At line : "+e.line+", column : "+e.column)}},dust.filterNode=function(a,b){return dust.optimizers[b[0]](a,b)},dust.optimizers={body:c,buffer:e,special:d,format:f,reference:b,"#":b,"?":b,"^":b,"<":b,"+":b,"@":b,"%":b,partial:b,context:b,params:b,bodies:b,param:b,filters:e,key:e,path:e,literal:e,comment:f,line:f,col:f},dust.pragmas={esc:function(a,b,c){var d,e=a.auto;return b||(b="h"),a.auto="s"===b?"":b,d=j(a,c.block),a.auto=e,d}};var l={s:" ",n:"\n",r:"\r",lb:"{",rb:"}"};dust.compileNode=function(a,b){return dust.nodes[b[0]](a,b)},dust.nodes={body:function(a,b){var c=a.index++,d="body_"+c;return a.bodies[c]=j(a,b),d},buffer:function(a,b){return".write("+m(b[1])+")"},format:function(a,b){return".write("+m(b[1]+b[2])+")"},reference:function(a,b){return".reference("+dust.compileNode(a,b[1])+",ctx,"+dust.compileNode(a,b[2])+")"},"#":function(a,b){return k(a,b,"section")},"?":function(a,b){return k(a,b,"exists")},"^":function(a,b){return k(a,b,"notexists")},"<":function(a,b){for(var c=b[4],d=1,e=c.length;e>d;d++){var f=c[d],g=f[1][1];if("block"===g)return a.blocks[b[1].text]=dust.compileNode(a,f[2]),""}return""},"+":function(a,b){return"undefined"==typeof b[1].text&&"undefined"==typeof b[4]?".block(ctx.getBlock("+dust.compileNode(a,b[1])+",chk, ctx),"+dust.compileNode(a,b[2])+", {},"+dust.compileNode(a,b[3])+")":".block(ctx.getBlock("+m(b[1].text)+"),"+dust.compileNode(a,b[2])+","+dust.compileNode(a,b[4])+","+dust.compileNode(a,b[3])+")"},"@":function(a,b){return".helper("+m(b[1].text)+","+dust.compileNode(a,b[2])+","+dust.compileNode(a,b[4])+","+dust.compileNode(a,b[3])+")"},"%":function(a,b){var c,d,e,f,g,h,i,j,k,l=b[1][1];if(!dust.pragmas[l])return"";for(c=b[4],d={},j=1,k=c.length;k>j;j++)h=c[j],d[h[1][1]]=h[2];for(e=b[3],f={},j=1,k=e.length;k>j;j++)i=e[j],f[i[1][1]]=i[2][1];return g=b[2][1]?b[2][1].text:null,dust.pragmas[l](a,g,d,f)},partial:function(a,b){return".partial("+dust.compileNode(a,b[1])+","+dust.compileNode(a,b[2])+","+dust.compileNode(a,b[3])+")"},context:function(a,b){return b[1]?"ctx.rebase("+dust.compileNode(a,b[1])+")":"ctx"},params:function(a,b){for(var c=[],d=1,e=b.length;e>d;d++)c.push(dust.compileNode(a,b[d]));return c.length?"{"+c.join(",")+"}":"null"},bodies:function(a,b){for(var c=[],d=1,e=b.length;e>d;d++)c.push(dust.compileNode(a,b[d]));return"{"+c.join(",")+"}"},param:function(a,b){return dust.compileNode(a,b[1])+":"+dust.compileNode(a,b[2])},filters:function(a,b){for(var c=[],d=1,e=b.length;e>d;d++){var f=b[d];c.push('"'+f+'"')}return'"'+a.auto+'"'+(c.length?",["+c.join(",")+"]":"")},key:function(a,b){return'ctx._get(false, ["'+b[1]+'"])'},path:function(a,b){for(var c=b[1],d=b[2],e=[],f=0,g=d.length;g>f;f++)dust.isArray(d[f])?e.push(dust.compileNode(a,d[f])):e.push('"'+d[f]+'"');return"ctx._get("+c+",["+e.join(",")+"])"},literal:function(a,b){return m(b[1])}};var m="undefined"==typeof JSON?function(a){return'"'+dust.escapeJs(a)+'"'}:JSON.stringify;return dust};"undefined"!=typeof exports?module.exports=dustCompiler:dustCompiler(getGlobal()),function(dust){var a=function(){function b(a){return'"'+a.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\x08/g,"\\b").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\f/g,"\\f").replace(/\r/g,"\\r").replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g,escape)+'"'}var c={parse:function(c,d){function e(a){var b={};for(var c in a)b[c]=a[c];return b}function f(a,b){for(var d=a.offset+b,e=a.offset;d>e;e++){var f=c.charAt(e);"\n"===f?(a.seenCR||a.line++,a.column=1,a.seenCR=!1):"\r"===f||"\u2028"===f||"\u2029"===f?(a.line++,a.column=1,a.seenCR=!0):(a.column++,a.seenCR=!1)}a.offset+=b}function g(a){Q.offset<S.offset||(Q.offset>S.offset&&(S=e(Q),T=[]),T.push(a))}function h(){var a,b,c;for(c=e(Q),a=[],b=i();null!==b;)a.push(b),b=i();return null!==a&&(a=function(a,b,c,d){return["body"].concat(d).concat([["line",b],["col",c]])}(c.offset,c.line,c.column,a)),null===a&&(Q=e(c)),a}function i(){var a;return a=G(),null===a&&(a=j(),null===a&&(a=q(),null===a&&(a=s(),null===a&&(a=p(),null===a&&(a=D()))))),a}function j(){var a,b,d,i,j,m,n,p,q;if(R++,p=e(Q),q=e(Q),a=k(),null!==a){for(b=[],d=N();null!==d;)b.push(d),d=N();null!==b?(d=J(),null!==d?(i=h(),null!==i?(j=o(),null!==j?(m=l(),m=null!==m?m:"",null!==m?(n=function(a,b,c,d,e,f,g){if(!g||d[1].text!==g.text)throw new Error("Expected end tag for "+d[1].text+" but it was not found. At line : "+b+", column : "+c);return!0}(Q.offset,Q.line,Q.column,a,i,j,m)?"":null,null!==n?a=[a,b,d,i,j,m,n]:(a=null,Q=e(q))):(a=null,Q=e(q))):(a=null,Q=e(q))):(a=null,Q=e(q))):(a=null,Q=e(q))):(a=null,Q=e(q))}else a=null,Q=e(q);if(null!==a&&(a=function(a,b,c,d,e,f){return f.push(["param",["literal","block"],e]),d.push(f),d.concat([["line",b],["col",c]])}(p.offset,p.line,p.column,a[0],a[3],a[4],a[5])),null===a&&(Q=e(p)),null===a){if(p=e(Q),q=e(Q),a=k(),null!==a){for(b=[],d=N();null!==d;)b.push(d),d=N();null!==b?(47===c.charCodeAt(Q.offset)?(d="/",f(Q,1)):(d=null,0===R&&g('"/"')),null!==d?(i=J(),null!==i?a=[a,b,d,i]:(a=null,Q=e(q))):(a=null,Q=e(q))):(a=null,Q=e(q))}else a=null,Q=e(q);null!==a&&(a=function(a,b,c,d){return d.push(["bodies"]),d.concat([["line",b],["col",c]])}(p.offset,p.line,p.column,a[0])),null===a&&(Q=e(p))}return R--,0===R&&null===a&&g("section"),a}function k(){var a,b,d,h,i,j,k,l;if(k=e(Q),l=e(Q),a=I(),null!==a)if(/^[#?^<+@%]/.test(c.charAt(Q.offset))?(b=c.charAt(Q.offset),f(Q,1)):(b=null,0===R&&g("[#?^<+@%]")),null!==b){for(d=[],h=N();null!==h;)d.push(h),h=N();null!==d?(h=t(),null!==h?(i=m(),null!==i?(j=n(),null!==j?a=[a,b,d,h,i,j]:(a=null,Q=e(l))):(a=null,Q=e(l))):(a=null,Q=e(l))):(a=null,Q=e(l))}else a=null,Q=e(l);else a=null,Q=e(l);return null!==a&&(a=function(a,b,c,d,e,f,g){return[d,e,f,g]}(k.offset,k.line,k.column,a[1],a[3],a[4],a[5])),null===a&&(Q=e(k)),a}function l(){var a,b,d,h,i,j,k,l;if(R++,k=e(Q),l=e(Q),a=I(),null!==a)if(47===c.charCodeAt(Q.offset)?(b="/",f(Q,1)):(b=null,0===R&&g('"/"')),null!==b){for(d=[],h=N();null!==h;)d.push(h),h=N();if(null!==d)if(h=t(),null!==h){for(i=[],j=N();null!==j;)i.push(j),j=N();null!==i?(j=J(),null!==j?a=[a,b,d,h,i,j]:(a=null,Q=e(l))):(a=null,Q=e(l))}else a=null,Q=e(l);else a=null,Q=e(l)}else a=null,Q=e(l);else a=null,Q=e(l);return null!==a&&(a=function(a,b,c,d){return d}(k.offset,k.line,k.column,a[3])),null===a&&(Q=e(k)),R--,0===R&&null===a&&g("end tag"),a}function m(){var a,b,d,h,i;return d=e(Q),h=e(Q),i=e(Q),58===c.charCodeAt(Q.offset)?(a=":",f(Q,1)):(a=null,0===R&&g('":"')),null!==a?(b=t(),null!==b?a=[a,b]:(a=null,Q=e(i))):(a=null,Q=e(i)),null!==a&&(a=function(a,b,c,d){return d}(h.offset,h.line,h.column,a[1])),null===a&&(Q=e(h)),a=null!==a?a:"",null!==a&&(a=function(a,b,c,d){return d?["context",d]:["context"]}(d.offset,d.line,d.column,a)),null===a&&(Q=e(d)),a}function n(){var a,b,d,h,i,j,k,l;if(R++,j=e(Q),a=[],k=e(Q),l=e(Q),d=N(),null!==d)for(b=[];null!==d;)b.push(d),d=N();else b=null;for(null!==b?(d=y(),null!==d?(61===c.charCodeAt(Q.offset)?(h="=",f(Q,1)):(h=null,0===R&&g('"="')),null!==h?(i=u(),null===i&&(i=t(),null===i&&(i=B())),null!==i?b=[b,d,h,i]:(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l)),null!==b&&(b=function(a,b,c,d,e){return["param",["literal",d],e]}(k.offset,k.line,k.column,b[1],b[3])),null===b&&(Q=e(k));null!==b;){if(a.push(b),k=e(Q),l=e(Q),d=N(),null!==d)for(b=[];null!==d;)b.push(d),d=N();else b=null;null!==b?(d=y(),null!==d?(61===c.charCodeAt(Q.offset)?(h="=",f(Q,1)):(h=null,0===R&&g('"="')),null!==h?(i=u(),null===i&&(i=t(),null===i&&(i=B())),null!==i?b=[b,d,h,i]:(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l)),null!==b&&(b=function(a,b,c,d,e){return["param",["literal",d],e]}(k.offset,k.line,k.column,b[1],b[3])),null===b&&(Q=e(k))}return null!==a&&(a=function(a,b,c,d){return["params"].concat(d)}(j.offset,j.line,j.column,a)),null===a&&(Q=e(j)),R--,0===R&&null===a&&g("params"),a}function o(){var a,b,d,i,j,k,l,m,n;for(R++,l=e(Q),a=[],m=e(Q),n=e(Q),b=I(),null!==b?(58===c.charCodeAt(Q.offset)?(d=":",f(Q,1)):(d=null,0===R&&g('":"')),null!==d?(i=y(),null!==i?(j=J(),null!==j?(k=h(),null!==k?b=[b,d,i,j,k]:(b=null,Q=e(n))):(b=null,Q=e(n))):(b=null,Q=e(n))):(b=null,Q=e(n))):(b=null,Q=e(n)),null!==b&&(b=function(a,b,c,d,e){return["param",["literal",d],e]}(m.offset,m.line,m.column,b[2],b[4])),null===b&&(Q=e(m));null!==b;)a.push(b),m=e(Q),n=e(Q),b=I(),null!==b?(58===c.charCodeAt(Q.offset)?(d=":",f(Q,1)):(d=null,0===R&&g('":"')),null!==d?(i=y(),null!==i?(j=J(),null!==j?(k=h(),null!==k?b=[b,d,i,j,k]:(b=null,Q=e(n))):(b=null,Q=e(n))):(b=null,Q=e(n))):(b=null,Q=e(n))):(b=null,Q=e(n)),null!==b&&(b=function(a,b,c,d,e){return["param",["literal",d],e]}(m.offset,m.line,m.column,b[2],b[4])),null===b&&(Q=e(m));return null!==a&&(a=function(a,b,c,d){return["bodies"].concat(d)}(l.offset,l.line,l.column,a)),null===a&&(Q=e(l)),R--,0===R&&null===a&&g("bodies"),a}function p(){var a,b,c,d,f,h;return R++,f=e(Q),h=e(Q),a=I(),null!==a?(b=t(),null!==b?(c=r(),null!==c?(d=J(),null!==d?a=[a,b,c,d]:(a=null,Q=e(h))):(a=null,Q=e(h))):(a=null,Q=e(h))):(a=null,Q=e(h)),null!==a&&(a=function(a,b,c,d,e){return["reference",d,e].concat([["line",b],["col",c]])}(f.offset,f.line,f.column,a[1],a[2])),null===a&&(Q=e(f)),R--,0===R&&null===a&&g("reference"),a}function q(){var a,b,d,h,i,j,k,l,o,p,q,r;if(R++,p=e(Q),q=e(Q),a=I(),null!==a)if(62===c.charCodeAt(Q.offset)?(b=">",f(Q,1)):(b=null,0===R&&g('">"')),null===b&&(43===c.charCodeAt(Q.offset)?(b="+",f(Q,1)):(b=null,0===R&&g('"+"'))),null!==b){for(d=[],h=N();null!==h;)d.push(h),h=N();if(null!==d)if(r=e(Q),h=y(),null!==h&&(h=function(a,b,c,d){return["literal",d]}(r.offset,r.line,r.column,h)),null===h&&(Q=e(r)),null===h&&(h=B()),null!==h)if(i=m(),null!==i)if(j=n(),null!==j){for(k=[],l=N();null!==l;)k.push(l),l=N();null!==k?(47===c.charCodeAt(Q.offset)?(l="/",f(Q,1)):(l=null,0===R&&g('"/"')),null!==l?(o=J(),null!==o?a=[a,b,d,h,i,j,k,l,o]:(a=null,Q=e(q))):(a=null,Q=e(q))):(a=null,Q=e(q))}else a=null,Q=e(q);else a=null,Q=e(q);else a=null,Q=e(q);else a=null,Q=e(q)}else a=null,Q=e(q);else a=null,Q=e(q);return null!==a&&(a=function(a,b,c,d,e,f,g){var h=">"===d?"partial":d;return[h,e,f,g].concat([["line",b],["col",c]])}(p.offset,p.line,p.column,a[1],a[3],a[4],a[5])),null===a&&(Q=e(p)),R--,0===R&&null===a&&g("partial"),a}function r(){var a,b,d,h,i,j;for(R++,h=e(Q),a=[],i=e(Q),j=e(Q),124===c.charCodeAt(Q.offset)?(b="|",f(Q,1)):(b=null,0===R&&g('"|"')),null!==b?(d=y(),null!==d?b=[b,d]:(b=null,Q=e(j))):(b=null,Q=e(j)),null!==b&&(b=function(a,b,c,d){return d}(i.offset,i.line,i.column,b[1])),null===b&&(Q=e(i));null!==b;)a.push(b),i=e(Q),j=e(Q),124===c.charCodeAt(Q.offset)?(b="|",f(Q,1)):(b=null,0===R&&g('"|"')),null!==b?(d=y(),null!==d?b=[b,d]:(b=null,Q=e(j))):(b=null,Q=e(j)),null!==b&&(b=function(a,b,c,d){return d}(i.offset,i.line,i.column,b[1])),null===b&&(Q=e(i));return null!==a&&(a=function(a,b,c,d){return["filters"].concat(d)}(h.offset,h.line,h.column,a)),null===a&&(Q=e(h)),R--,0===R&&null===a&&g("filters"),a}function s(){var a,b,d,h,i,j;return R++,i=e(Q),j=e(Q),a=I(),null!==a?(126===c.charCodeAt(Q.offset)?(b="~",f(Q,1)):(b=null,0===R&&g('"~"')),null!==b?(d=y(),null!==d?(h=J(),null!==h?a=[a,b,d,h]:(a=null,Q=e(j))):(a=null,Q=e(j))):(a=null,Q=e(j))):(a=null,Q=e(j)),null!==a&&(a=function(a,b,c,d){return["special",d].concat([["line",b],["col",c]])}(i.offset,i.line,i.column,a[2])),null===a&&(Q=e(i)),R--,0===R&&null===a&&g("special"),a}function t(){var a,b;return R++,b=e(Q),a=x(),null!==a&&(a=function(a,b,c,d){var e=["path"].concat(d);return e.text=d[1].join("."),e}(b.offset,b.line,b.column,a)),null===a&&(Q=e(b)),null===a&&(b=e(Q),a=y(),null!==a&&(a=function(a,b,c,d){var e=["key",d];return e.text=d,e}(b.offset,b.line,b.column,a)),null===a&&(Q=e(b))),R--,0===R&&null===a&&g("identifier"),a}function u(){var a,b;return R++,b=e(Q),a=v(),null===a&&(a=w()),null!==a&&(a=function(a,b,c,d){return["literal",d]}(b.offset,b.line,b.column,a)),null===a&&(Q=e(b)),R--,0===R&&null===a&&g("number"),a}function v(){var a,b,d,h,i,j;if(R++,i=e(Q),j=e(Q),a=w(),null!==a)if(46===c.charCodeAt(Q.offset)?(b=".",f(Q,1)):(b=null,0===R&&g('"."')),null!==b){if(h=w(),null!==h)for(d=[];null!==h;)d.push(h),h=w();else d=null;null!==d?a=[a,b,d]:(a=null,Q=e(j))}else a=null,Q=e(j);else a=null,Q=e(j);return null!==a&&(a=function(a,b,c,d,e){return parseFloat(d+"."+e.join(""))}(i.offset,i.line,i.column,a[0],a[2])),null===a&&(Q=e(i)),R--,0===R&&null===a&&g("float"),a}function w(){var a,b,d;if(R++,d=e(Q),/^[0-9]/.test(c.charAt(Q.offset))?(b=c.charAt(Q.offset),f(Q,1)):(b=null,0===R&&g("[0-9]")),null!==b)for(a=[];null!==b;)a.push(b),/^[0-9]/.test(c.charAt(Q.offset))?(b=c.charAt(Q.offset),f(Q,1)):(b=null,0===R&&g("[0-9]"));else a=null;return null!==a&&(a=function(a,b,c,d){return parseInt(d.join(""),10)}(d.offset,d.line,d.column,a)),null===a&&(Q=e(d)),R--,0===R&&null===a&&g("integer"),a}function x(){var a,b,d,h,i;if(R++,h=e(Q),i=e(Q),a=y(),a=null!==a?a:"",null!==a){if(d=A(),null===d&&(d=z()),null!==d)for(b=[];null!==d;)b.push(d),d=A(),null===d&&(d=z());else b=null;null!==b?a=[a,b]:(a=null,Q=e(i))}else a=null,Q=e(i);if(null!==a&&(a=function(a,b,c,d,e){return e=e[0],d&&e?(e.unshift(d),[!1,e].concat([["line",b],["col",c]])):[!0,e].concat([["line",b],["col",c]])}(h.offset,h.line,h.column,a[0],a[1])),null===a&&(Q=e(h)),null===a){if(h=e(Q),i=e(Q),46===c.charCodeAt(Q.offset)?(a=".",f(Q,1)):(a=null,0===R&&g('"."')),null!==a){for(b=[],d=A(),null===d&&(d=z());null!==d;)b.push(d),d=A(),null===d&&(d=z());null!==b?a=[a,b]:(a=null,Q=e(i))}else a=null,Q=e(i);null!==a&&(a=function(a,b,c,d){return d.length>0?[!0,d[0]].concat([["line",b],["col",c]]):[!0,[]].concat([["line",b],["col",c]])}(h.offset,h.line,h.column,a[1])),null===a&&(Q=e(h))}return R--,0===R&&null===a&&g("path"),a}function y(){var a,b,d,h,i;if(R++,h=e(Q),i=e(Q),/^[a-zA-Z_$]/.test(c.charAt(Q.offset))?(a=c.charAt(Q.offset),f(Q,1)):(a=null,0===R&&g("[a-zA-Z_$]")),null!==a){for(b=[],/^[0-9a-zA-Z_$\-]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g("[0-9a-zA-Z_$\\-]"));null!==d;)b.push(d),/^[0-9a-zA-Z_$\-]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g("[0-9a-zA-Z_$\\-]"));null!==b?a=[a,b]:(a=null,Q=e(i))}else a=null,Q=e(i);return null!==a&&(a=function(a,b,c,d,e){return d+e.join("")}(h.offset,h.line,h.column,a[0],a[1])),null===a&&(Q=e(h)),R--,0===R&&null===a&&g("key"),a}function z(){var a,b,d,h,i,j,k,l;if(R++,h=e(Q),i=e(Q),j=e(Q),k=e(Q),a=K(),null!==a){if(l=e(Q),/^[0-9]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g("[0-9]")),null!==d)for(b=[];null!==d;)b.push(d),/^[0-9]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g("[0-9]"));else b=null;null!==b&&(b=function(a,b,c,d){return d.join("")}(l.offset,l.line,l.column,b)),null===b&&(Q=e(l)),null===b&&(b=t()),null!==b?(d=L(),null!==d?a=[a,b,d]:(a=null,Q=e(k))):(a=null,Q=e(k))}else a=null,Q=e(k);return null!==a&&(a=function(a,b,c,d){return d}(j.offset,j.line,j.column,a[1])),null===a&&(Q=e(j)),null!==a?(b=A(),b=null!==b?b:"",null!==b?a=[a,b]:(a=null,Q=e(i))):(a=null,Q=e(i)),null!==a&&(a=function(a,b,c,d,e){return e?e.unshift(d):e=[d],e}(h.offset,h.line,h.column,a[0],a[1])),null===a&&(Q=e(h)),R--,0===R&&null===a&&g("array"),a}function A(){var a,b,d,h,i,j,k;if(R++,h=e(Q),i=e(Q),j=e(Q),k=e(Q),46===c.charCodeAt(Q.offset)?(b=".",f(Q,1)):(b=null,0===R&&g('"."')),null!==b?(d=y(),null!==d?b=[b,d]:(b=null,Q=e(k))):(b=null,Q=e(k)),null!==b&&(b=function(a,b,c,d){return d}(j.offset,j.line,j.column,b[1])),null===b&&(Q=e(j)),null!==b)for(a=[];null!==b;)a.push(b),j=e(Q),k=e(Q),46===c.charCodeAt(Q.offset)?(b=".",f(Q,1)):(b=null,0===R&&g('"."')),null!==b?(d=y(),null!==d?b=[b,d]:(b=null,Q=e(k))):(b=null,Q=e(k)),null!==b&&(b=function(a,b,c,d){return d}(j.offset,j.line,j.column,b[1])),null===b&&(Q=e(j));else a=null;return null!==a?(b=z(),b=null!==b?b:"",null!==b?a=[a,b]:(a=null,Q=e(i))):(a=null,Q=e(i)),null!==a&&(a=function(a,b,c,d,e){return e?d.concat(e):d}(h.offset,h.line,h.column,a[0],a[1])),null===a&&(Q=e(h)),R--,0===R&&null===a&&g("array_part"),a}function B(){var a,b,d,h,i;if(R++,h=e(Q),i=e(Q),34===c.charCodeAt(Q.offset)?(a='"',f(Q,1)):(a=null,0===R&&g('"\\""')),null!==a?(34===c.charCodeAt(Q.offset)?(b='"',f(Q,1)):(b=null,0===R&&g('"\\""')),null!==b?a=[a,b]:(a=null,Q=e(i))):(a=null,Q=e(i)),null!==a&&(a=function(a,b,c){return["literal",""].concat([["line",b],["col",c]])}(h.offset,h.line,h.column)),null===a&&(Q=e(h)),null===a&&(h=e(Q),i=e(Q),34===c.charCodeAt(Q.offset)?(a='"',f(Q,1)):(a=null,0===R&&g('"\\""')),null!==a?(b=E(),null!==b?(34===c.charCodeAt(Q.offset)?(d='"',f(Q,1)):(d=null,0===R&&g('"\\""')),null!==d?a=[a,b,d]:(a=null,Q=e(i))):(a=null,Q=e(i))):(a=null,Q=e(i)),null!==a&&(a=function(a,b,c,d){return["literal",d].concat([["line",b],["col",c]])}(h.offset,h.line,h.column,a[1])),null===a&&(Q=e(h)),null===a)){if(h=e(Q),i=e(Q),34===c.charCodeAt(Q.offset)?(a='"',f(Q,1)):(a=null,0===R&&g('"\\""')),null!==a){if(d=C(),null!==d)for(b=[];null!==d;)b.push(d),d=C();else b=null;null!==b?(34===c.charCodeAt(Q.offset)?(d='"',f(Q,1)):(d=null,0===R&&g('"\\""')),null!==d?a=[a,b,d]:(a=null,Q=e(i))):(a=null,Q=e(i))}else a=null,Q=e(i);null!==a&&(a=function(a,b,c,d){return["body"].concat(d).concat([["line",b],["col",c]])}(h.offset,h.line,h.column,a[1])),null===a&&(Q=e(h))}return R--,0===R&&null===a&&g("inline"),a}function C(){var a,b;return a=s(),null===a&&(a=p(),null===a&&(b=e(Q),a=E(),null!==a&&(a=function(a,b,c,d){return["buffer",d]}(b.offset,b.line,b.column,a)),null===a&&(Q=e(b)))),a}function D(){var a,b,d,h,i,j,k,l,m;if(R++,j=e(Q),k=e(Q),a=M(),null!==a){for(b=[],d=N();null!==d;)b.push(d),d=N();null!==b?a=[a,b]:(a=null,Q=e(k))}else a=null,Q=e(k);if(null!==a&&(a=function(a,b,c,d,e){return["format",d,e.join("")].concat([["line",b],["col",c]])}(j.offset,j.line,j.column,a[0],a[1])),null===a&&(Q=e(j)),null===a){if(j=e(Q),k=e(Q),l=e(Q),m=e(Q),R++,b=H(),R--,null===b?b="":(b=null,Q=e(m)),null!==b?(m=e(Q),R++,d=G(),R--,null===d?d="":(d=null,Q=e(m)),null!==d?(m=e(Q),R++,h=M(),R--,null===h?h="":(h=null,Q=e(m)),null!==h?(c.length>Q.offset?(i=c.charAt(Q.offset),f(Q,1)):(i=null,0===R&&g("any character")),null!==i?b=[b,d,h,i]:(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l)),null!==b&&(b=function(a,b,c,d){return d}(k.offset,k.line,k.column,b[3])),null===b&&(Q=e(k)),null!==b)for(a=[];null!==b;)a.push(b),k=e(Q),l=e(Q),m=e(Q),R++,b=H(),R--,null===b?b="":(b=null,Q=e(m)),null!==b?(m=e(Q),R++,d=G(),R--,null===d?d="":(d=null,Q=e(m)),null!==d?(m=e(Q),R++,h=M(),R--,null===h?h="":(h=null,Q=e(m)),null!==h?(c.length>Q.offset?(i=c.charAt(Q.offset),f(Q,1)):(i=null,0===R&&g("any character")),null!==i?b=[b,d,h,i]:(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l))):(b=null,Q=e(l)),null!==b&&(b=function(a,b,c,d){return d}(k.offset,k.line,k.column,b[3])),null===b&&(Q=e(k));else a=null;null!==a&&(a=function(a,b,c,d){return["buffer",d.join("")].concat([["line",b],["col",c]])}(j.offset,j.line,j.column,a)),null===a&&(Q=e(j))}return R--,0===R&&null===a&&g("buffer"),a}function E(){var a,b,d,h,i,j,k;if(R++,h=e(Q),i=e(Q),j=e(Q),k=e(Q),R++,b=H(),R--,null===b?b="":(b=null,Q=e(k)),null!==b?(d=F(),null===d&&(/^[^"]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g('[^"]'))),null!==d?b=[b,d]:(b=null,Q=e(j))):(b=null,Q=e(j)),null!==b&&(b=function(a,b,c,d){return d}(i.offset,i.line,i.column,b[1])),null===b&&(Q=e(i)),null!==b)for(a=[];null!==b;)a.push(b),i=e(Q),j=e(Q),k=e(Q),R++,b=H(),R--,null===b?b="":(b=null,Q=e(k)),null!==b?(d=F(),null===d&&(/^[^"]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g('[^"]'))),null!==d?b=[b,d]:(b=null,Q=e(j))):(b=null,Q=e(j)),null!==b&&(b=function(a,b,c,d){return d}(i.offset,i.line,i.column,b[1])),null===b&&(Q=e(i));else a=null;return null!==a&&(a=function(a,b,c,d){return d.join("")}(h.offset,h.line,h.column,a)),null===a&&(Q=e(h)),R--,0===R&&null===a&&g("literal"),a}function F(){var a,b;return b=e(Q),'\\"'===c.substr(Q.offset,2)?(a='\\"',f(Q,2)):(a=null,0===R&&g('"\\\\\\""')),null!==a&&(a=function(){return'"'}(b.offset,b.line,b.column)),null===a&&(Q=e(b)),a}function G(){var a,b,d,h,i,j,k,l,m;if(R++,i=e(Q),j=e(Q),"{!"===c.substr(Q.offset,2)?(a="{!",f(Q,2)):(a=null,0===R&&g('"{!"')),null!==a){for(b=[],k=e(Q),l=e(Q),m=e(Q),R++,"!}"===c.substr(Q.offset,2)?(d="!}",f(Q,2)):(d=null,0===R&&g('"!}"')),R--,null===d?d="":(d=null,Q=e(m)),null!==d?(c.length>Q.offset?(h=c.charAt(Q.offset),f(Q,1)):(h=null,0===R&&g("any character")),null!==h?d=[d,h]:(d=null,Q=e(l))):(d=null,Q=e(l)),null!==d&&(d=function(a,b,c,d){return d
+}(k.offset,k.line,k.column,d[1])),null===d&&(Q=e(k));null!==d;)b.push(d),k=e(Q),l=e(Q),m=e(Q),R++,"!}"===c.substr(Q.offset,2)?(d="!}",f(Q,2)):(d=null,0===R&&g('"!}"')),R--,null===d?d="":(d=null,Q=e(m)),null!==d?(c.length>Q.offset?(h=c.charAt(Q.offset),f(Q,1)):(h=null,0===R&&g("any character")),null!==h?d=[d,h]:(d=null,Q=e(l))):(d=null,Q=e(l)),null!==d&&(d=function(a,b,c,d){return d}(k.offset,k.line,k.column,d[1])),null===d&&(Q=e(k));null!==b?("!}"===c.substr(Q.offset,2)?(d="!}",f(Q,2)):(d=null,0===R&&g('"!}"')),null!==d?a=[a,b,d]:(a=null,Q=e(j))):(a=null,Q=e(j))}else a=null,Q=e(j);return null!==a&&(a=function(a,b,c,d){return["comment",d.join("")].concat([["line",b],["col",c]])}(i.offset,i.line,i.column,a[1])),null===a&&(Q=e(i)),R--,0===R&&null===a&&g("comment"),a}function H(){var a,b,d,h,i,j,k,l,m,n,o;if(m=e(Q),a=I(),null!==a){for(b=[],d=N();null!==d;)b.push(d),d=N();if(null!==b)if(/^[#?^><+%:@\/~%]/.test(c.charAt(Q.offset))?(d=c.charAt(Q.offset),f(Q,1)):(d=null,0===R&&g("[#?^><+%:@\\/~%]")),null!==d){for(h=[],i=N();null!==i;)h.push(i),i=N();if(null!==h){if(n=e(Q),o=e(Q),R++,j=J(),R--,null===j?j="":(j=null,Q=e(o)),null!==j?(o=e(Q),R++,k=M(),R--,null===k?k="":(k=null,Q=e(o)),null!==k?(c.length>Q.offset?(l=c.charAt(Q.offset),f(Q,1)):(l=null,0===R&&g("any character")),null!==l?j=[j,k,l]:(j=null,Q=e(n))):(j=null,Q=e(n))):(j=null,Q=e(n)),null!==j)for(i=[];null!==j;)i.push(j),n=e(Q),o=e(Q),R++,j=J(),R--,null===j?j="":(j=null,Q=e(o)),null!==j?(o=e(Q),R++,k=M(),R--,null===k?k="":(k=null,Q=e(o)),null!==k?(c.length>Q.offset?(l=c.charAt(Q.offset),f(Q,1)):(l=null,0===R&&g("any character")),null!==l?j=[j,k,l]:(j=null,Q=e(n))):(j=null,Q=e(n))):(j=null,Q=e(n));else i=null;if(null!==i){for(j=[],k=N();null!==k;)j.push(k),k=N();null!==j?(k=J(),null!==k?a=[a,b,d,h,i,j,k]:(a=null,Q=e(m))):(a=null,Q=e(m))}else a=null,Q=e(m)}else a=null,Q=e(m)}else a=null,Q=e(m);else a=null,Q=e(m)}else a=null,Q=e(m);return null===a&&(a=p()),a}function I(){var a;return 123===c.charCodeAt(Q.offset)?(a="{",f(Q,1)):(a=null,0===R&&g('"{"')),a}function J(){var a;return 125===c.charCodeAt(Q.offset)?(a="}",f(Q,1)):(a=null,0===R&&g('"}"')),a}function K(){var a;return 91===c.charCodeAt(Q.offset)?(a="[",f(Q,1)):(a=null,0===R&&g('"["')),a}function L(){var a;return 93===c.charCodeAt(Q.offset)?(a="]",f(Q,1)):(a=null,0===R&&g('"]"')),a}function M(){var a;return 10===c.charCodeAt(Q.offset)?(a="\n",f(Q,1)):(a=null,0===R&&g('"\\n"')),null===a&&("\r\n"===c.substr(Q.offset,2)?(a="\r\n",f(Q,2)):(a=null,0===R&&g('"\\r\\n"')),null===a&&(13===c.charCodeAt(Q.offset)?(a="\r",f(Q,1)):(a=null,0===R&&g('"\\r"')),null===a&&(8232===c.charCodeAt(Q.offset)?(a="\u2028",f(Q,1)):(a=null,0===R&&g('"\\u2028"')),null===a&&(8233===c.charCodeAt(Q.offset)?(a="\u2029",f(Q,1)):(a=null,0===R&&g('"\\u2029"')))))),a}function N(){var a;return/^[\t\x0B\f \xA0\uFEFF]/.test(c.charAt(Q.offset))?(a=c.charAt(Q.offset),f(Q,1)):(a=null,0===R&&g("[\\t\\x0B\\f \\xA0\\uFEFF]")),null===a&&(a=M()),a}function O(a){a.sort();for(var b=null,c=[],d=0;d<a.length;d++)a[d]!==b&&(c.push(a[d]),b=a[d]);return c}var P={body:h,part:i,section:j,sec_tag_start:k,end_tag:l,context:m,params:n,bodies:o,reference:p,partial:q,filters:r,special:s,identifier:t,number:u,"float":v,integer:w,path:x,key:y,array:z,array_part:A,inline:B,inline_part:C,buffer:D,literal:E,esc:F,comment:G,tag:H,ld:I,rd:J,lb:K,rb:L,eol:M,ws:N};if(void 0!==d){if(void 0===P[d])throw new Error("Invalid rule name: "+b(d)+".")}else d="body";var Q={offset:0,line:1,column:1,seenCR:!1},R=0,S={offset:0,line:1,column:1,seenCR:!1},T=[],U=P[d]();if(null===U||Q.offset!==c.length){var V=Math.max(Q.offset,S.offset),W=V<c.length?c.charAt(V):null,X=Q.offset>S.offset?Q:S;throw new a.SyntaxError(O(T),W,V,X.line,X.column)}return U},toSource:function(){return this._source}};return c.SyntaxError=function(a,c,d,e,f){function g(a,c){var d,e;switch(a.length){case 0:d="end of input";break;case 1:d=a[0];break;default:d=a.slice(0,a.length-1).join(", ")+" or "+a[a.length-1]}return e=c?b(c):"end of input","Expected "+d+" but "+e+" found."}this.name="SyntaxError",this.expected=a,this.found=c,this.message=g(a,c),this.offset=d,this.line=e,this.column=f},c.SyntaxError.prototype=Error.prototype,c}();dust.parse=a.parse}("undefined"!=typeof exports?exports:getGlobal());
\ No newline at end of file