azkaban-memoizeit

Details

diff --git a/src/java/azkaban/executor/ExecutableFlow.java b/src/java/azkaban/executor/ExecutableFlow.java
index 883a19f..b449654 100644
--- a/src/java/azkaban/executor/ExecutableFlow.java
+++ b/src/java/azkaban/executor/ExecutableFlow.java
@@ -34,6 +34,7 @@ import azkaban.utils.JSONUtils;
 public class ExecutableFlow {
 	private int executionId = -1;
 	private String flowId;
+	private int scheduleId = -1;
 	private int projectId;
 	private int version;
 
@@ -57,6 +58,7 @@ public class ExecutableFlow {
 	
 	public ExecutableFlow(Flow flow) {
 		this.projectId = flow.getProjectId();
+		this.scheduleId = -1;
 		this.flowId = flow.getId();
 		this.version = flow.getVersion();
 		this.setFlow(flow);
@@ -64,6 +66,7 @@ public class ExecutableFlow {
 	
 	public ExecutableFlow(int executionId, Flow flow) {
 		this.projectId = flow.getProjectId();
+		this.scheduleId = -1;
 		this.flowId = flow.getId();
 		this.version = flow.getVersion();
 		this.executionId = executionId;
@@ -208,6 +211,14 @@ public class ExecutableFlow {
 		this.projectId = projectId;
 	}
 
+	public int getScheduleId() {
+		return scheduleId;
+	}
+
+	public void setScheduleId(int scheduleId) {
+		this.scheduleId = scheduleId;
+	}
+
 	public String getExecutionPath() {
 		return executionPath;
 	}
@@ -255,6 +266,9 @@ public class ExecutableFlow {
 		flowObj.put("executionPath", executionPath);
 		flowObj.put("flowId", flowId);
 		flowObj.put("projectId", projectId);
+		if(scheduleId >= 0) {
+			flowObj.put("scheduleId", scheduleId);
+		}
 		flowObj.put("submitTime", submitTime);
 		flowObj.put("startTime", startTime);
 		flowObj.put("endTime", endTime);
@@ -370,6 +384,9 @@ public class ExecutableFlow {
 		exFlow.executionPath = (String)flowObj.get("executionPath");
 		exFlow.flowId = (String)flowObj.get("flowId");
 		exFlow.projectId = (Integer)flowObj.get("projectId");
+		if (flowObj.containsKey("scheduleId")) {
+			exFlow.scheduleId = (Integer)flowObj.get("scheduleId");
+		}
 		exFlow.submitTime = JSONUtils.getLongFromObject(flowObj.get("submitTime"));
 		exFlow.startTime = JSONUtils.getLongFromObject(flowObj.get("startTime"));
 		exFlow.endTime = JSONUtils.getLongFromObject(flowObj.get("endTime"));
diff --git a/src/java/azkaban/executor/JdbcExecutorLoader.java b/src/java/azkaban/executor/JdbcExecutorLoader.java
index 3390074..33a9f76 100644
--- a/src/java/azkaban/executor/JdbcExecutorLoader.java
+++ b/src/java/azkaban/executor/JdbcExecutorLoader.java
@@ -881,7 +881,8 @@ public class JdbcExecutorLoader extends AbstractJdbcLoader implements ExecutorLo
 		try {
 			updateNum = runner.update(DELETE_BY_TIME, millis);
 		} catch (SQLException e) {
-			throw new ExecutorManagerException("Error deleting old execution_logs before " + millis);
+			e.printStackTrace();
+			throw new ExecutorManagerException("Error deleting old execution_logs before " + millis, e);			
 		}
 		
 		return updateNum;
diff --git a/src/java/azkaban/jobtype/JobTypeManager.java b/src/java/azkaban/jobtype/JobTypeManager.java
index 3bfc51d..0526c4a 100644
--- a/src/java/azkaban/jobtype/JobTypeManager.java
+++ b/src/java/azkaban/jobtype/JobTypeManager.java
@@ -90,11 +90,19 @@ public class JobTypeManager
 	// load Job Typs from dir
 	private void loadPluginJobTypes() throws JobTypeManagerException
 	{
-		if(jobtypePluginDir == null || parentLoader == null) throw new JobTypeManagerException("JobTypeDir not set! JobTypeManager not properly initiated!");
-		
 		File jobPluginsDir = new File(jobtypePluginDir);
-		if(!jobPluginsDir.isDirectory()) throw new JobTypeManagerException("Job type plugin dir " + jobtypePluginDir + " is not a directory!");
-		if(!jobPluginsDir.canRead()) throw new JobTypeManagerException("Job type plugin dir " + jobtypePluginDir + " is not readable!");
+		
+		if (!jobPluginsDir.exists()) {
+			return;
+		}
+
+		if (!jobPluginsDir.isDirectory()) {
+			throw new JobTypeManagerException("Job type plugin dir " + jobtypePluginDir + " is not a directory!");
+		}
+		
+		if (!jobPluginsDir.canRead()) {
+			throw new JobTypeManagerException("Job type plugin dir " + jobtypePluginDir + " is not readable!");
+		}
 		
 		// look for global conf
 		Props globalConf = null;
diff --git a/src/java/azkaban/scheduler/JdbcScheduleLoader.java b/src/java/azkaban/scheduler/JdbcScheduleLoader.java
index 39a6cf5..809aeef 100644
--- a/src/java/azkaban/scheduler/JdbcScheduleLoader.java
+++ b/src/java/azkaban/scheduler/JdbcScheduleLoader.java
@@ -46,19 +46,19 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 	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;
+			"SELECT schedule_id, 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=?";
+			"DELETE FROM " + scheduleTableName + " WHERE schedule_id=?";
 	
 	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=?";
+			"UPDATE " + scheduleTableName + " SET status=?, first_sched_time=?, timezone=?, period=?, last_modify_time=?, next_exec_time=?, submit_time=?, submit_user=?, enc_type=?, schedule_options=? WHERE schedule_id=?";
 	
 	private static String UPDATE_NEXT_EXEC_TIME = 
-			"UPDATE " + scheduleTableName + " SET next_exec_time=? WHERE project_id=? AND flow_name=?";
+			"UPDATE " + scheduleTableName + " SET next_exec_time=? WHERE schedule_id=?";
 
 	public EncodingType getDefaultEncodingType() {
 		return defaultEncodingType;
@@ -127,7 +127,7 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 
 		QueryRunner runner = createQueryRunner();
 		try {
-			int removes =  runner.update(REMOVE_SCHEDULE_BY_KEY, s.getProjectId(), s.getFlowName());
+			int removes =  runner.update(REMOVE_SCHEDULE_BY_KEY, s.getScheduleId());
 			if (removes == 0) {
 				throw new ScheduleManagerException("No schedule has been removed.");
 			}
@@ -177,6 +177,15 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 					s.getSubmitUser(),
 					encType.getNumVal(),
 					data);
+			
+			long id = runner.query(LastInsertID.LAST_INSERT_ID, new LastInsertID());
+
+			if (id == -1l) {
+				throw new ScheduleManagerException("Execution id is not properly created.");
+			}
+			logger.info("Schedule given " + s.getScheduleIdentityPair() + " given id " + id);
+			s.setScheduleId((int)id);
+			
 			if (inserts == 0) {
 				throw new ScheduleManagerException("No schedule has been inserted.");
 			}
@@ -194,7 +203,7 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 		QueryRunner runner = new QueryRunner();
 		try {
 			
-			runner.update(connection, UPDATE_NEXT_EXEC_TIME, s.getNextExecTime(), s.getProjectId(), s.getFlowName()); 
+			runner.update(connection, UPDATE_NEXT_EXEC_TIME, s.getNextExecTime(), s.getScheduleId()); 
 		} catch (SQLException e) {
 			e.printStackTrace();
 			logger.error(UPDATE_NEXT_EXEC_TIME + " failed.", e);
@@ -242,8 +251,7 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 					s.getSubmitUser(), 	
 					encType.getNumVal(),
 					data,
-					s.getProjectId(), 
-					s.getFlowName());
+					s.getScheduleId());
 			if (updates == 0) {
 				throw new ScheduleManagerException("No schedule has been updated.");
 			}
@@ -253,6 +261,22 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 		}
 	}
 
+	
+	private static class LastInsertID implements ResultSetHandler<Long> {
+		private static String LAST_INSERT_ID = "SELECT LAST_INSERT_ID()";
+		
+		@Override
+		public Long handle(ResultSet rs) throws SQLException {
+			if (!rs.next()) {
+				return -1l;
+			}
+
+			long id = rs.getLong(1);
+			return id;
+		}
+		
+	}
+	
 	public class ScheduleResultHandler implements ResultSetHandler<List<Schedule>> {
 		@Override
 		public List<Schedule> handle(ResultSet rs) throws SQLException {
@@ -262,19 +286,20 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 			
 			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);
+				int scheduleId = rs.getInt(1);
+				int projectId = rs.getInt(2);
+				String projectName = rs.getString(3);
+				String flowName = rs.getString(4);
+				String status = rs.getString(5);
+				long firstSchedTime = rs.getLong(6);
+				DateTimeZone timezone = DateTimeZone.forID(rs.getString(7));
+				ReadablePeriod period = Schedule.parsePeriodString(rs.getString(8));
+				long lastModifyTime = rs.getLong(9);
+				long nextExecTime = rs.getLong(10);
+				long submitTime = rs.getLong(11);
+				String submitUser = rs.getString(12);
+				int encodingType = rs.getInt(13);
+				byte[] data = rs.getBytes(14);
 				
 				Object optsObj = null;
 				if (data != null) {
@@ -296,7 +321,7 @@ public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLo
 					}
 				}
 				
-				Schedule s = new Schedule(projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser);
+				Schedule s = new Schedule(scheduleId, projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser);
 				if (optsObj != null) {
 					s.createAndSetScheduleOptions(optsObj);
 				}
diff --git a/src/java/azkaban/scheduler/Schedule.java b/src/java/azkaban/scheduler/Schedule.java
index b27cc96..f11c0fe 100644
--- a/src/java/azkaban/scheduler/Schedule.java
+++ b/src/java/azkaban/scheduler/Schedule.java
@@ -40,7 +40,8 @@ public class Schedule{
 //	private long flowGuid;
 	
 //	private String scheduleId;
-	
+
+	private int scheduleId;
 	private int projectId;
 	private String projectName;
 	private String flowName;
@@ -57,6 +58,7 @@ public class Schedule{
 	private SlaOptions slaOptions;
 	
 	public Schedule(
+						int scheduleId,
 						int projectId,
 						String projectName,
 						String flowName,
@@ -69,22 +71,26 @@ public class Schedule{
 						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;
+
+		this(scheduleId, 
+				projectId, 
+				projectName, 
+				flowName, 
+				status,
+				firstSchedTime, 
+				timezone,
+				period,
+				lastModifyTime,
+				nextExecTime,
+				submitTime,
+				submitUser,
+				null,
+				null
+				);
 	}
 
 	public Schedule(
+						int scheduleId,
 						int projectId,
 						String projectName,
 						String flowName,
@@ -99,22 +105,24 @@ public class Schedule{
 						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;
+		this(scheduleId, projectId, 
+				projectName, 
+				flowName, 
+				status,
+				firstSchedTime, 
+				DateTimeZone.forID(timezoneId),
+				parsePeriodString(period),
+				lastModifyTime,
+				nextExecTime,
+				submitTime,
+				submitUser,
+				executionOptions,
+				slaOptions
+				);
 	}
 
 	public Schedule(
+						int scheduleId,
 						int projectId,
 						String projectName,
 						String flowName,
@@ -129,6 +137,7 @@ public class Schedule{
 						ExecutionOptions executionOptions,
 						SlaOptions slaOptions
 						) {
+		this.scheduleId = scheduleId;
 		this.projectId = projectId;
 		this.projectName = projectName;
 		this.flowName = flowName;
@@ -169,9 +178,17 @@ public class Schedule{
 				new DateTime(firstSchedTime).toDateTimeISO() + " with recurring period of " + (period == null ? "non-recurring" : createPeriodString(period));
 	}
 	
-	public Pair<Integer, String> getScheduleId() {
+	public Pair<Integer, String> getScheduleIdentityPair() {
 		return new Pair<Integer, String>(getProjectId(), getFlowName());
 	}
+
+	public void setScheduleId(int scheduleId) {
+		this.scheduleId = scheduleId;
+	}
+	
+	public int getScheduleId() {
+		return scheduleId;
+	}
 	
 	public int getProjectId() {
 		return projectId;
diff --git a/src/java/azkaban/scheduler/ScheduleManager.java b/src/java/azkaban/scheduler/ScheduleManager.java
index 5688f66..de90a5c 100644
--- a/src/java/azkaban/scheduler/ScheduleManager.java
+++ b/src/java/azkaban/scheduler/ScheduleManager.java
@@ -19,9 +19,11 @@ package azkaban.scheduler;
 import java.lang.Thread.State;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.PriorityBlockingQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -36,15 +38,13 @@ import azkaban.executor.ExecutableFlow;
 import azkaban.executor.ExecutionOptions;
 import azkaban.executor.ExecutorManager;
 import azkaban.executor.ExecutorManagerException;
-
 import azkaban.flow.Flow;
 import azkaban.project.Project;
 import azkaban.project.ProjectManager;
-
 import azkaban.sla.SLA.SlaAction;
 import azkaban.sla.SLA.SlaRule;
-import azkaban.sla.SLAManager;
 import azkaban.sla.SLA.SlaSetting;
+import azkaban.sla.SLAManager;
 import azkaban.sla.SlaOptions;
 import azkaban.utils.Pair;
 
@@ -60,7 +60,8 @@ public class ScheduleManager {
 	private final DateTimeFormatter _dateFormat = DateTimeFormat.forPattern("MM-dd-yyyy HH:mm:ss:SSS");
 	private ScheduleLoader loader;
 
-	private Map<Pair<Integer, String>, Schedule> scheduleIDMap = new LinkedHashMap<Pair<Integer, String>, Schedule>();
+	private Map<Pair<Integer, String>, Set<Schedule>> scheduleIdentityPairMap = new LinkedHashMap<Pair<Integer, String>, Set<Schedule>>();
+	private Map<Integer, Schedule> scheduleIDMap = new LinkedHashMap<Integer, Schedule>();
 	private final ScheduleRunner runner;
 	private final ExecutorManager executorManager;
 	private final ProjectManager projectManager;
@@ -126,20 +127,48 @@ public class ScheduleManager {
 	 * @param id
 	 * @return
 	*/
-	public Schedule getSchedule(int projectId, String flowId) {
-		return scheduleIDMap.get(new Pair<Integer,String>(projectId, flowId));
+	public Set<Schedule> getSchedules(int projectId, String flowId) {
+		return scheduleIdentityPairMap.get(new Pair<Integer,String>(projectId, flowId));
+	}
+
+	/**
+	 * Returns the scheduled flow for the scheduleId
+	 * 
+	 * @param id
+	 * @return
+	*/
+	public Schedule getSchedule(int scheduleId) {
+		return scheduleIDMap.get(scheduleId);
 	}
 
+
 	/**
 	 * Removes the flow from the schedule if it exists.
 	 * 
 	 * @param id
 	 */
-	public synchronized void removeSchedule(int projectId, String flowId) {
-		Pair<Integer,String> scheduleId = new Pair<Integer,String>(projectId, flowId);
-		
-		Schedule sched = scheduleIDMap.get(scheduleId);
-		scheduleIDMap.remove(scheduleId);
+	public synchronized void removeSchedules(int projectId, String flowId) {
+		Set<Schedule> schedules = getSchedules(projectId, flowId);
+		for(Schedule sched : schedules) {
+			removeSchedule(sched);
+		}
+	}
+	/**
+	 * Removes the flow from the schedule if it exists.
+	 * 
+	 * @param id
+	 */
+	public synchronized void removeSchedule(Schedule sched) {
+
+		Pair<Integer,String> identityPairMap = new Pair<Integer,String>(sched.getProjectId(), sched.getFlowName());
+		Set<Schedule> schedules = scheduleIdentityPairMap.get(identityPairMap);
+		if(schedules != null) {
+			schedules.remove(sched);
+			if(schedules.size() == 0) {
+				scheduleIdentityPairMap.remove(identityPairMap);
+			}
+		}
+		scheduleIDMap.remove(sched.getScheduleId());
 		
 		runner.removeRunnerSchedule(sched);
 		try {
@@ -173,6 +202,7 @@ public class ScheduleManager {
 	// }
 
 	public Schedule scheduleFlow(
+			final int scheduleId,
 			final int projectId,
 			final String projectName,
 			final String flowName,
@@ -185,10 +215,11 @@ public class ScheduleManager {
 			final long submitTime,
 			final String submitUser
 			) {
-		return scheduleFlow(projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser, null, null);
+		return scheduleFlow(scheduleId, projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser, null, null);
 	}
 	
 	public Schedule scheduleFlow(
+			final int scheduleId,
 			final int projectId,
 			final String projectName,
 			final String flowName,
@@ -203,7 +234,7 @@ public class ScheduleManager {
 			ExecutionOptions execOptions,
 			SlaOptions slaOptions
 			) {
-		Schedule sched = new Schedule(projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser, execOptions, slaOptions);
+		Schedule sched = new Schedule(scheduleId, projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser, execOptions, slaOptions);
 		logger.info("Scheduling flow '" + sched.getScheduleName() + "' for "
 				+ _dateFormat.print(firstSchedTime) + " with a period of "
 				+ period == null ? "(non-recurring)" : period);
@@ -225,6 +256,12 @@ public class ScheduleManager {
 		s.updateTime();
 		this.runner.addRunnerSchedule(s);
 		scheduleIDMap.put(s.getScheduleId(), s);
+		Set<Schedule> schedules = scheduleIdentityPairMap.get(s.getScheduleIdentityPair());
+		if(schedules == null) {
+			schedules = new HashSet<Schedule>();
+			scheduleIdentityPairMap.put(s.getScheduleIdentityPair(), schedules);
+		}
+		schedules.add(s);
 	}
 
 	/**
@@ -233,7 +270,7 @@ public class ScheduleManager {
 	 * @param flow
 	 */
 	public synchronized void insertSchedule(Schedule s) {
-		boolean exist = scheduleIDMap.containsKey(s.getScheduleId());
+		boolean exist = scheduleIdentityPairMap.containsKey(s.getScheduleIdentityPair());
 		if(s.updateTime()) {
 			internalSchedule(s);
 			try {
@@ -370,6 +407,7 @@ public class ScheduleManager {
 
 									// Create ExecutableFlow
 									ExecutableFlow exflow = new ExecutableFlow(flow);
+									exflow.setScheduleId(runningSched.getScheduleId());
 									exflow.setSubmitUser(runningSched.getSubmitUser());
 									exflow.addAllProxyUsers(project.getProxyUsers());
 									
@@ -441,7 +479,7 @@ public class ScheduleManager {
 									loader.updateSchedule(runningSched);
 								}
 								else {
-									removeSchedule(runningSched.getProjectId(), runningSched.getFlowName());
+									removeSchedule(runningSched);
 								}								
 							} else {
 								// wait until flow run
diff --git a/src/java/azkaban/webapp/servlet/ExecutorServlet.java b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
index ea19307..f10b43e 100644
--- a/src/java/azkaban/webapp/servlet/ExecutorServlet.java
+++ b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
@@ -567,7 +567,7 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 		ret.put("nodeStatus", nodeStatus);
 		ret.put("disabled", options.getDisabledJobs());
 		
-		Schedule sflow = scheduleManager.getSchedule(project.getId(), exflow.getFlowId());
+		Schedule sflow = null;// = scheduleManager.getSchedule(project.getId(), exflow.getFlowId());
 		
 		for (Schedule sched: scheduleManager.getSchedules()) {
 			if (sched.getProjectId() == project.getId() && sched.getFlowName().equals(exflow.getFlowId())) {
diff --git a/src/java/azkaban/webapp/servlet/ScheduleServlet.java b/src/java/azkaban/webapp/servlet/ScheduleServlet.java
index b236c56..4f4bd32 100644
--- a/src/java/azkaban/webapp/servlet/ScheduleServlet.java
+++ b/src/java/azkaban/webapp/servlet/ScheduleServlet.java
@@ -76,6 +76,9 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 		if (hasParam(req, "ajax")) {
 			handleAJAXAction(req, resp, session);
 		}
+		else if (hasParam(req, "calendar")) {
+			handleGetScheduleCalendar(req, resp, session);
+		}
 		else {
 			handleGetAllSchedules(req, resp, session);
 		}
@@ -91,6 +94,9 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 		else if(ajaxName.equals("setSla")) {
 			ajaxSetSla(req, ret, session.getUser());
 		}
+		else if(ajaxName.equals("loadFlow")) {
+			ajaxLoadFlows(req, ret, session.getUser());
+		}
 		else if(ajaxName.equals("scheduleFlow")) {
 			ajaxScheduleFlow(req, ret, session.getUser());
 		}
@@ -103,16 +109,16 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 	private void ajaxSetSla(HttpServletRequest req, HashMap<String, Object> ret, User user) {
 		try {
 			
-			int projectId = getIntParam(req, "projectId");
-			String flowName = getParam(req, "flowName");
+			int scheduleId = getIntParam(req, "scheduleId");
+			
+			Schedule sched = scheduleManager.getSchedule(scheduleId);
 			
-			Project project = projectManager.getProject(projectId);
+			Project project = projectManager.getProject(sched.getProjectId());
 			if(!hasPermission(project, user, Permission.Type.SCHEDULE)) {
 				ret.put("error", "User " + user + " does not have permission to set SLA for this flow.");
 				return;
 			}
 			
-			Schedule sched = scheduleManager.getSchedule(projectId, flowName);
 			
 			SlaOptions slaOptions= new SlaOptions();
 			
@@ -149,7 +155,7 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 			scheduleManager.insertSchedule(sched);
 
 			if(slaOptions != null) {
-				projectManager.postProjectEvent(project, EventType.SLA, user.getUserId(), "SLA for flow " + flowName + " has been added/changed.");
+				projectManager.postProjectEvent(project, EventType.SLA, user.getUserId(), "SLA for flow " + sched.getFlowName() + " has been added/changed.");
 			}
 			
 		} catch (ServletException e) {
@@ -198,26 +204,24 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 	}
 
 	private void ajaxSlaInfo(HttpServletRequest req, HashMap<String, Object> ret, User user) {
-		int projId;
-		String flowName;
+		int scheduleId;
 		try {
-			projId = getIntParam(req, "projId");
-			flowName = getParam(req, "flowName");
+			scheduleId = getIntParam(req, "scheduleId");
 			
-			Project project = getProjectAjaxByPermission(ret, projId, user, Type.READ);
+			Schedule sched = scheduleManager.getSchedule(scheduleId);
+			
+			Project project = getProjectAjaxByPermission(ret, sched.getProjectId(), user, Type.READ);
 			if (project == null) {
-				ret.put("error", "Error loading project. Project " + projId + " doesn't exist");
+				ret.put("error", "Error loading project. Project " + sched.getProjectId() + " doesn't exist");
 				return;
 			}
 			
-			Flow flow = project.getFlow(flowName);
+			Flow flow = project.getFlow(sched.getFlowName());
 			if (flow == null) {
-				ret.put("error", "Error loading flow. Flow " + flowName + " doesn't exist in " + projId);
+				ret.put("error", "Error loading flow. Flow " + sched.getFlowName() + " doesn't exist in " + sched.getProjectId());
 				return;
 			}
 			
-			Schedule sched = scheduleManager.getSchedule(projId, flowName);
-			
 			SlaOptions slaOptions = sched.getSlaOptions();
 			ExecutionOptions flowOptions = sched.getExecutionOptions();
 			
@@ -298,6 +302,20 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 		page.render();
 	}
 	
+	private void handleGetScheduleCalendar(HttpServletRequest req, HttpServletResponse resp,
+			Session session) throws ServletException, IOException{
+		
+		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm");
+		
+		List<Schedule> schedules = scheduleManager.getSchedules();
+		page.add("schedules", schedules);
+//		
+//		List<SLA> slas = slaManager.getSLAs();
+//		page.add("slas", slas);
+
+		page.render();
+	}
+	
 	@Override
 	protected void handlePost(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
 		if (hasParam(req, "ajax")) {
@@ -323,34 +341,124 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 			this.writeJSON(resp, ret);
 		}
 	}
-	
-	private void ajaxRemoveSched(HttpServletRequest req, Map<String, Object> ret, User user) throws ServletException{
-		int projectId = getIntParam(req, "projectId");
-		String flowName = getParam(req, "flowName");
-		Schedule sched = scheduleManager.getSchedule(projectId, flowName);
 
-//		int projectId = sched.getProjectId();
+	private void ajaxLoadFlows(HttpServletRequest req, HashMap<String, Object> ret, User user) throws ServletException {
+		// Very long day...
+//		long day = getLongParam(req, "day");
+//		boolean loadPrevious = getIntParam(req, "loadPrev") != 0;
 
-		Project project = projectManager.getProject(projectId);
+		List<Schedule> schedules = scheduleManager.getSchedules();
+		// See if anything is scheduled
+		if (schedules.size() <= 0)
+			return;
+//
+//		// Since size is larger than 0, there's at least one element.
+//		DateTime date = new DateTime(day);
+//		// Get only the day component while stripping the time component. This
+//		// gives us 12:00:00AM of that day
+//		DateTime start = date.withTime(0, 0, 0, 0);
+//		// Next day
+//		DateTime end = start.plusDays(1);
+//		// Get microseconds
+//		long startTime = start.getMillis();
+//		long endTime = end.getMillis();
+
+		List<HashMap<String, String>> output = new ArrayList<HashMap<String, String>>();
+		ret.put("items", output);
+
+		for (Schedule schedule : schedules) {
+			writeScheduleData(output, schedule);
+//			long length = 2*3600*1000; //TODO: This is temporary
+//			long firstTime = schedule.getFirstSchedTime();
+//			long period = 0;
+//
+//			if (schedule.getPeriod() != null) {
+//				period = start.plus(schedule.getPeriod()).getMillis() - startTime;
+//
+//				// Shift time until we're past the start time
+//				if (period > 0) {
+//					// Calculate next execution time efficiently
+//					long periods = (startTime - firstTime) / period;
+//					// Take into account items that ends in the date specified, but does not start on that date
+//					if(loadPrevious)
+//					{
+//						periods = (startTime - firstTime - length) / period;
+//					}
+//					if(periods < 0){
+//						periods = 0;
+//					}
+//					firstTime += period * periods;
+//					// Increment in case we haven't arrived yet. This will apply
+//					// to most of the cases
+//					while ((loadPrevious && firstTime < startTime) || (!loadPrevious && firstTime + length < startTime)) {
+//						firstTime += period;
+//					}
+//				}
+//			}
+//
+//			// Bad or no period
+//			if (period <= 0) {
+//				// Single instance case
+//				if (firstTime >= startTime && firstTime < endTime) {
+//					writeScheduleData(output, schedule, firstTime, length, startTime, endTime);
+//				}
+//			}
+//			else {
+//				// Repetitive schedule, firstTime is assumed to be after startTime
+//				while (firstTime < endTime) {
+//					writeScheduleData(output, schedule, firstTime, length, startTime, endTime);
+//					firstTime += period;
+//				}
+//			}
+		}
+	}
+
+	private void writeScheduleData(List<HashMap<String, String>> output, Schedule schedule) {
+		HashMap<String, String> data = new HashMap<String, String>();
+		data.put("flowname", schedule.getFlowName());
+		data.put("projectname", schedule.getProjectName());
+		data.put("time", Long.toString(schedule.getFirstSchedTime()));
+
+		DateTime time = DateTime.now();
+		long period = 0;
+		if(schedule.getPeriod() != null){
+			period = time.plus(schedule.getPeriod()).getMillis() - time.getMillis();
+		}
+		data.put("period", Long.toString(period));
+		data.put("length", Long.toString(2 * 3600 * 1000));
+
+		output.add(data);
+	}
+
+	private void ajaxRemoveSched(HttpServletRequest req, Map<String, Object> ret, User user) throws ServletException{
+		int scheduleId = getIntParam(req, "scheduleId");
+		Schedule sched = scheduleManager.getSchedule(scheduleId);
+		if(sched == null) {
+			ret.put("message", "Schedule with ID " + scheduleId + " does not exist");
+			ret.put("status", "error");
+			return;
+		}
+
+		Project project = projectManager.getProject(sched.getProjectId());
 		
 		if (project == null) {
-			ret.put("message", "Project " + projectId + " does not exist");
+			ret.put("message", "Project " + sched.getProjectId() + " does not exist");
 			ret.put("status", "error");
 			return;
 		}
 		
 		if(!hasPermission(project, user, Type.SCHEDULE)) {
 			ret.put("status", "error");
-			ret.put("message", "Permission denied. Cannot remove schedule " + projectId + "."  + flowName);
+			ret.put("message", "Permission denied. Cannot remove schedule with id " + scheduleId);
 			return;
 		}
 
-		scheduleManager.removeSchedule(projectId, flowName);
+		scheduleManager.removeSchedule(sched);
 		logger.info("User '" + user.getUserId() + " has removed schedule " + sched.getScheduleName());
 		projectManager.postProjectEvent(project, EventType.SCHEDULE, user.getUserId(), "Schedule " + sched.toString() + " has been removed.");
 		
 		ret.put("status", "success");
-		ret.put("message", "flow " + flowName + " removed from Schedules.");
+		ret.put("message", "flow " + sched.getFlowName() + " removed from Schedules.");
 		return;
 	}
 
@@ -401,7 +509,7 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 			ret.put("error", e.getMessage());
 		}
 		
-		Schedule sched = scheduleManager.getSchedule(projectId, flowName);
+		// Schedule sched = scheduleManager.getSchedule(projectId, flowName);
 		ExecutionOptions flowOptions = null;
 		try {
 			flowOptions = HttpRequestUtils.parseFlowOptions(req);
@@ -410,12 +518,12 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 			ret.put("error", e.getMessage());
 		}
 		SlaOptions slaOptions = null;
-		if(sched != null) {
-			if(sched.getSlaOptions() != null) {
-				slaOptions = sched.getSlaOptions();
-			}
-		}
-		Schedule schedule = scheduleManager.scheduleFlow(projectId, projectName, flowName, "ready", firstSchedTime.getMillis(), firstSchedTime.getZone(), thePeriod, DateTime.now().getMillis(), firstSchedTime.getMillis(), firstSchedTime.getMillis(), user.getUserId(), flowOptions, slaOptions);
+		//		if(sched != null) {
+		//			if(sched.getSlaOptions() != null) {
+		//				slaOptions = sched.getSlaOptions();
+		//			}
+		//		}
+		Schedule schedule = scheduleManager.scheduleFlow(-1, projectId, projectName, flowName, "ready", firstSchedTime.getMillis(), firstSchedTime.getZone(), thePeriod, DateTime.now().getMillis(), firstSchedTime.getMillis(), firstSchedTime.getMillis(), user.getUserId(), flowOptions, slaOptions);
 		logger.info("User '" + user.getUserId() + "' has scheduled " + "[" + projectName + flowName +  " (" + projectId +")" + "].");
 		projectManager.postProjectEvent(project, EventType.SCHEDULE, user.getUserId(), "Schedule " + schedule.toString() + " has been added.");
 
diff --git a/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm b/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
new file mode 100644
index 0000000..2652467
--- /dev/null
+++ b/src/java/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
@@ -0,0 +1,84 @@
+#*
+ * 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.
+*#
+
+<!DOCTYPE html> 
+<html>
+	<head>
+#parse( "azkaban/webapp/servlet/velocity/style.vm" )
+		<link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
+		<link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui.css" />
+		
+		<script type="text/javascript" src="${context}/js/jquery/jquery-1.9.1.js"></script>    
+		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-1.10.1.custom.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/backbone-0.9.10-min.js"></script>
+		<script type="text/javascript" src="${context}/js/jquery.simplemodal-1.4.4.js"></script>
+		<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.nav.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">
+			var contextURL = "${context}";
+			var currentTime = ${currentTime};
+			var timezone = "${timezone}";
+			var errorMessage = null;
+			var successMessage = null;
+		</script>
+		<link rel="stylesheet" type="text/css" href="${context}/css/jquery.svg.css" />
+	</head>
+	<body>
+#set($current_page="schedule")
+#parse( "azkaban/webapp/servlet/velocity/nav.vm" )
+		<div class="messaging"><p id="messageClose">X</p><p id="message"></p></div>  
+
+		<div class="content">
+		
+#if($errorMsg)
+		<div class="box-error-message">$errorMsg</div>
+#else
+#if($error_message != "null")
+		<div class="box-error-message">$error_message</div>
+#elseif($success_message != "null")
+		<div class="box-success-message">$success_message</div>
+#end
+#end		
+		
+		<div id="all-scheduledFlows-content">
+			<div class="section-hd">
+				<h2>Scheduled Flows</h2>
+			</div>
+		</div>
+		
+		<div class="scheduledFlows">
+			<span class="nav-prev-week btn1" style="margin: 20px; margin-left: 50px;"><a>Previous Week</a></span>
+			<span class="nav-this-week btn1" style="margin: 20px;"><a>Today</a></span>
+			<span class="nav-next-week btn1" style="margin: 20px; margin-right: 50px;"><a>Next Week</a></span>
+			<div id="svgDivCustom">
+			
+			</div>
+		</div>
+		</div>
+		
+		<div id="contextMenu">
+		</div>
+	</body>
+</html>
diff --git a/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
index f65bdfb..4e2fbd8 100644
--- a/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
@@ -69,6 +69,7 @@
 					<thead>
 						<tr>
 							<!--th class="execid">Execution Id</th-->
+							<th>ID</th>
 							<th>Flow</th>
 							<th>Project</th>
 							<th>Submitted By</th>
@@ -84,6 +85,7 @@
 #foreach($sched in $schedules)
 						<tr class="row" >
 
+							<td>${sched.scheduleId}</td>
 							<td class="tb-name">
 								<a href="${context}/manager?project=${sched.projectName}&flow=${sched.flowName}">${sched.flowName}</a>
 							</td>
@@ -95,8 +97,8 @@
 							<td>$utils.formatDateTime(${sched.nextExecTime})</td>
 							<td>$utils.formatPeriod(${sched.period})</td>
 							<td>#if(${sched.slaOptions}) true #else false #end</td>
-							<td><button id="removeSchedBtn" onclick="removeSched(${sched.projectId}, '${sched.flowName}')" >Remove Schedule</button></td>
-							<td><button id="addSlaBtn" onclick="slaView.initFromSched(${sched.projectId}, '${sched.flowName}')" >Set SLA</button></td>
+							<td><button id="removeSchedBtn" onclick="removeSched(${sched.scheduleId})" >Remove Schedule</button></td>
+							<td><button id="addSlaBtn" onclick="slaView.initFromSched(${sched.scheduleId}, '${sched.flowName}')" >Set SLA</button></td>
 						</tr>
 #end
 #else

src/sql/create_all.sql 170(+170 -0)

diff --git a/src/sql/create_all.sql b/src/sql/create_all.sql
new file mode 100644
index 0000000..14f67d4
--- /dev/null
+++ b/src/sql/create_all.sql
@@ -0,0 +1,170 @@
+CREATE TABLE active_executing_flows (
+	exec_id INT,
+	host VARCHAR(255),
+	port INT,
+	update_time BIGINT,
+	PRIMARY KEY (exec_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE execution_flows (
+	exec_id INT NOT NULL AUTO_INCREMENT,
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	flow_id VARCHAR(128) NOT NULL,
+	status TINYINT,
+	submit_user VARCHAR(64),
+	submit_time BIGINT,
+	update_time BIGINT,
+	start_time BIGINT,
+	end_time BIGINT,
+	enc_type TINYINT,
+	flow_data LONGBLOB,
+	PRIMARY KEY (exec_id),
+	INDEX start_time (start_time),
+	INDEX end_time (end_time),
+	INDEX time_range (start_time, end_time),
+	INDEX (project_id, flow_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE execution_jobs (
+	exec_id INT NOT NULL,
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	flow_id VARCHAR(128) NOT NULL,
+	job_id VARCHAR(128) NOT NULL,
+	attempt INT,
+	start_time BIGINT,
+	end_time BIGINT,
+	status TINYINT,
+	input_params LONGBLOB,
+	output_params LONGBLOB,
+	attachments LONGBLOB,
+	PRIMARY KEY (exec_id, job_id, attempt),
+	INDEX exec_job (exec_id, job_id),
+	INDEX exec_id (exec_id),
+	INDEX job_id (project_id, job_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE execution_logs (
+	exec_id INT NOT NULL,
+	name VARCHAR(128),
+	attempt INT,
+	enc_type TINYINT,
+	start_byte INT,
+	end_byte INT,
+	log LONGBLOB,
+	upload_time BIGINT,
+	PRIMARY KEY (exec_id, name, attempt, start_byte),
+	INDEX log_attempt (exec_id, name, attempt),
+	INDEX log_index (exec_id, name)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_events (
+	project_id INT NOT NULL,
+	event_type TINYINT NOT NULL,
+	event_time BIGINT NOT NULL,
+	username VARCHAR(64),
+	message VARCHAR(512),
+	INDEX log (project_id, event_time)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_files (
+	project_id INT NOT NULL,
+	version INT not NULL,
+	chunk INT,
+	size INT,
+	file LONGBLOB,
+	PRIMARY KEY (project_id, version, chunk),
+	INDEX file_version (project_id, version)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_flows (
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	flow_id VARCHAR(128),
+	modified_time BIGINT NOT NULL,
+	encoding_type TINYINT,
+	json BLOB,
+	PRIMARY KEY (project_id, version, flow_id),
+	INDEX (project_id, version)
+) ENGINE=InnoDB;
+
+
+CREATE TABLE project_permissions (
+	project_id VARCHAR(64) NOT NULL,
+	modified_time BIGINT NOT NULL,
+	name VARCHAR(64) NOT NULL,
+	permissions INT NOT NULL,
+	isGroup BOOLEAN NOT NULL,
+	PRIMARY KEY (project_id, name),
+	INDEX project_id (project_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_properties (
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	name VARCHAR(128),
+	modified_time BIGINT NOT NULL,
+	encoding_type TINYINT,
+	property BLOB,
+	PRIMARY KEY (project_id, version, name),
+	INDEX (project_id, version)
+) ENGINE=InnoDB;
+
+CREATE TABLE projects (
+	id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
+	name VARCHAR(64) NOT NULL,
+	active BOOLEAN,
+	modified_time BIGINT NOT NULL,
+	create_time BIGINT NOT NULL,
+	version INT,
+	last_modified_by VARCHAR(64) NOT NULL,
+	description VARCHAR(255),
+	enc_type TINYINT,
+	settings_blob LONGBLOB,
+	UNIQUE INDEX project_id (id),
+	INDEX project_name (name)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_versions (
+	project_id INT NOT NULL,
+	version INT not NULL,
+	upload_time BIGINT NOT NULL,
+	uploader VARCHAR(64) NOT NULL,
+	file_type VARCHAR(16),
+	file_name VARCHAR(128),
+	md5 BINARY(16),
+	num_chunks INT,
+	PRIMARY KEY (project_id, version),
+	INDEX project_version_id (project_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE schedules (
+	project_id INT NOT NULL,
+	project_name VARCHAR(128) NOT NULL,
+	flow_name VARCHAR(128) NOT NULL,
+	status VARCHAR(16),
+	first_sched_time BIGINT,
+	timezone VARCHAR(64),
+	period VARCHAR(16),
+	last_modify_time BIGINT,
+	next_exec_time BIGINT,
+	submit_time BIGINT,
+	submit_user VARCHAR(128),
+	enc_type TINYINT,
+	schedule_options LONGBLOB,
+	primary key(project_id, flow_name)
+) ENGINE=InnoDB;
+
+
+CREATE TABLE active_sla (
+	exec_id INT NOT NULL,
+	job_name VARCHAR(128) NOT NULL,
+	check_time BIGINT NOT NULL,
+	rule TINYINT NOT NULL,
+	enc_type TINYINT,
+	options LONGBLOB NOT NULL,
+	primary key(exec_id, job_name)
+) ENGINE=InnoDB;
+
+
diff --git a/src/sql/create_all.sql~ b/src/sql/create_all.sql~
new file mode 100644
index 0000000..70deb54
--- /dev/null
+++ b/src/sql/create_all.sql~
@@ -0,0 +1,168 @@
+CREATE TABLE active_executing_flows (
+	exec_id INT,
+	host VARCHAR(255),
+	port INT,
+	update_time BIGINT,
+	PRIMARY KEY (exec_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE execution_flows (
+	exec_id INT NOT NULL AUTO_INCREMENT,
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	flow_id VARCHAR(128) NOT NULL,
+	status TINYINT,
+	submit_user VARCHAR(64),
+	submit_time BIGINT,
+	update_time BIGINT,
+	start_time BIGINT,
+	end_time BIGINT,
+	enc_type TINYINT,
+	flow_data LONGBLOB,
+	PRIMARY KEY (exec_id),
+	INDEX start_time (start_time),
+	INDEX end_time (end_time),
+	INDEX time_range (start_time, end_time),
+	INDEX (project_id, flow_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE execution_jobs (
+	exec_id INT NOT NULL,
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	flow_id VARCHAR(128) NOT NULL,
+	job_id VARCHAR(128) NOT NULL,
+	attempt INT,
+	start_time BIGINT,
+	end_time BIGINT,
+	status TINYINT,
+	input_params LONGBLOB,
+	output_params LONGBLOB,
+	attachments LONGBLOB,
+	PRIMARY KEY (exec_id, job_id, attempt),
+	INDEX exec_job (exec_id, job_id),
+	INDEX exec_id (exec_id),
+	INDEX job_id (project_id, job_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE execution_logs (
+	exec_id INT NOT NULL,
+	name VARCHAR(128),
+	attempt INT,
+	enc_type TINYINT,
+	start_byte INT,
+	end_byte INT,
+	log LONGBLOB,
+	upload_time BIGINT,
+	PRIMARY KEY (exec_id, name, attempt, start_byte),
+	INDEX log_attempt (exec_id, name, attempt),
+	INDEX log_index (exec_id, name)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_events (
+	project_id INT NOT NULL,
+	event_type TINYINT NOT NULL,
+	event_time BIGINT NOT NULL,
+	username VARCHAR(64),
+	message VARCHAR(512),
+	INDEX log (project_id, event_time)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_files (
+	project_id INT NOT NULL,
+	version INT not NULL,
+	chunk INT,
+	size INT,
+	file LONGBLOB,
+	PRIMARY KEY (project_id, version, chunk),
+	INDEX file_version (project_id, version)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_flows (
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	flow_id VARCHAR(128),
+	modified_time BIGINT NOT NULL,
+	encoding_type TINYINT,
+	json BLOB,
+	PRIMARY KEY (project_id, version, flow_id),
+	INDEX (project_id, version)
+) ENGINE=InnoDB;
+
+
+CREATE TABLE project_permissions (
+	project_id VARCHAR(64) NOT NULL,
+	modified_time BIGINT NOT NULL,
+	name VARCHAR(64) NOT NULL,
+	permissions INT NOT NULL,
+	isGroup BOOLEAN NOT NULL,
+	PRIMARY KEY (project_id, name),
+	INDEX project_id (project_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_properties (
+	project_id INT NOT NULL,
+	version INT NOT NULL,
+	name VARCHAR(128),
+	modified_time BIGINT NOT NULL,
+	encoding_type TINYINT,
+	property BLOB,
+	PRIMARY KEY (project_id, version, name),
+	INDEX (project_id, version)
+) ENGINE=InnoDB;
+
+CREATE TABLE projects (
+	id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
+	name VARCHAR(64) NOT NULL,
+	active BOOLEAN,
+	modified_time BIGINT NOT NULL,
+	create_time BIGINT NOT NULL,
+	version INT,
+	last_modified_by VARCHAR(64) NOT NULL,
+	description VARCHAR(255),
+	enc_type TINYINT,
+	settings_blob LONGBLOB,
+	UNIQUE INDEX project_id (id),
+	INDEX project_name (name)
+) ENGINE=InnoDB;
+
+CREATE TABLE project_versions (
+	project_id INT NOT NULL,
+	version INT not NULL,
+	upload_time BIGINT NOT NULL,
+	uploader VARCHAR(64) NOT NULL,
+	file_type VARCHAR(16),
+	file_name VARCHAR(128),
+	md5 BINARY(16),
+	num_chunks INT,
+	PRIMARY KEY (project_id, version),
+	INDEX project_version_id (project_id)
+) ENGINE=InnoDB;
+
+CREATE TABLE schedules (
+	project_id INT NOT NULL,
+	project_name VARCHAR(128) NOT NULL,
+	flow_name VARCHAR(128) NOT NULL,
+	status VARCHAR(16),
+	first_sched_time BIGINT,
+	timezone VARCHAR(64),
+	period VARCHAR(16),
+	last_modify_time BIGINT,
+	next_exec_time BIGINT,
+	submit_time BIGINT,
+	submit_user VARCHAR(128),
+	enc_type TINYINT,
+	schedule_options LONGBLOB,
+	primary key(project_id, flow_name)
+) ENGINE=InnoDB;
+
+
+CREATE TABLE active_sla (
+	exec_id INT NOT NULL,
+	job_name VARCHAR(128) NOT NULL,
+	check_time BIGINT NOT NULL,
+	rule TINYINT NOT NULL,
+	enc_type TINYINT,
+	options LONGBLOB NOT NULL,
+	primary key(exec_id, job_name)
+) ENGINE=InnoDB;
diff --git a/src/web/css/jquery.svg.css b/src/web/css/jquery.svg.css
new file mode 100644
index 0000000..1ff48b1
--- /dev/null
+++ b/src/web/css/jquery.svg.css
@@ -0,0 +1,15 @@
+/* http://keith-wood.name/svg.html
+   SVG for jQuery v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+
+svg\:svg {
+	display: none;
+}
+
+.svg_error {
+	color: red;
+	font-weight: bold;
+}
diff --git a/src/web/js/#azkaban.schedule.svg.js# b/src/web/js/#azkaban.schedule.svg.js#
new file mode 100644
index 0000000..2e7ea56
--- /dev/null
+++ b/src/web/js/#azkaban.schedule.svg.js#
@@ -0,0 +1,342 @@
+$.namespace('azkaban');
+function removeSched(projectId, flowName) {
+	var scheduleURL = contextURL + "/schedule"
+	var redirectURL = contextURL + "/schedule"
+	$.post(
+			scheduleURL,
+			{"action":"removeSched", "projectId":projectId, "flowName":flowName},
+			function(data) {
+				if (data.error) {
+//                 alert(data.error)
+					$('#errorMsg').text(data.error);
+				}
+				else {
+// 		 alert("Schedule "+schedId+" removed!")
+					window.location = redirectURL;
+				}
+			},
+			"json"
+	)
+}
+
+function removeSla(projectId, flowName) {
+	var scheduleURL = contextURL + "/schedule"
+	var redirectURL = contextURL + "/schedule"
+	$.post(
+			scheduleURL,
+			{"action":"removeSla", "projectId":projectId, "flowName":flowName},
+			function(data) {
+				if (data.error) {
+//                 alert(data.error)
+					$('#errorMsg').text(data.error)
+				}
+				else {
+// 		 alert("Schedule "+schedId+" removed!")
+					window.location = redirectURL
+				}
+			},
+			"json"
+	)
+}
+
+azkaban.ChangeSlaView = Backbone.View.extend({
+	events : {
+		"click" : "closeEditingTarget",
+		"click #set-sla-btn": "handleSetSla",	
+		"click #remove-sla-btn": "handleRemoveSla",
+		"click #sla-cancel-btn": "handleSlaCancel",
+		"click .modal-close": "handleSlaCancel",
+		"click #addRow": "handleAddRow"
+	},
+	initialize: function(setting) {
+
+	},
+	handleSlaCancel: function(evt) {
+		console.log("Clicked cancel button");
+		var scheduleURL = contextURL + "/schedule";
+
+		$('#slaModalBackground').hide();
+		$('#sla-options').hide();
+		
+		var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+		var rows = tFlowRules.rows;
+		var rowLength = rows.length
+		for(var i = 0; i < rowLength-1; i++) {
+			tFlowRules.deleteRow(0);
+		}
+		
+	},
+	initFromSched: function(projId, flowName) {
+		this.projectId = projId;
+		this.flowName = flowName;
+		
+		var scheduleURL = contextURL + "/schedule"
+		this.scheduleURL = scheduleURL;
+		var indexToName = {};
+		var nameToIndex = {};
+		var indexToText = {};
+		this.indexToName = indexToName;
+		this.nameToIndex = nameToIndex;
+		this.indexToText = indexToText;
+		var ruleBoxOptions = ["SUCCESS", "FINISH"];
+		this.ruleBoxOptions = ruleBoxOptions;
+		
+		var fetchScheduleData = {"projId": this.projectId, "ajax":"slaInfo", "flowName":this.flowName};
+		
+		$.get(
+				this.scheduleURL,
+				fetchScheduleData,
+				function(data) {
+					if (data.error) {
+						alert(data.error);
+					}
+					else {
+						if (data.slaEmails) {
+							$('#slaEmails').val(data.slaEmails.join());
+						}
+						
+						var allJobNames = data.allJobNames;
+						
+						indexToName[0] = "";
+						nameToIndex[flowName] = 0;
+						indexToText[0] = "flow " + flowName;
+						for(var i = 1; i <= allJobNames.length; i++) {
+							indexToName[i] = allJobNames[i-1];
+							nameToIndex[allJobNames[i-1]] = i;
+							indexToText[i] = "job " + allJobNames[i-1];
+						}
+						
+						
+						
+						
+						
+						// populate with existing settings
+						if(data.settings) {
+							
+							var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+							
+							for(var setting in data.settings) {
+								var rFlowRule = tFlowRules.insertRow(0);
+								
+								var cId = rFlowRule.insertCell(-1);
+								var idSelect = document.createElement("select");
+								for(var i in indexToName) {
+									idSelect.options[i] = new Option(indexToText[i], indexToName[i]);
+									if(data.settings[setting].id == indexToName[i]) {
+										idSelect.options[i].selected = true;
+									}
+								}								
+								cId.appendChild(idSelect);
+								
+								var cRule = rFlowRule.insertCell(-1);
+								var ruleSelect = document.createElement("select");
+								for(var i in ruleBoxOptions) {
+									ruleSelect.options[i] = new Option(ruleBoxOptions[i], ruleBoxOptions[i]);
+									if(data.settings[setting].rule == ruleBoxOptions[i]) {
+										ruleSelect.options[i].selected = true;
+									}
+								}
+								cRule.appendChild(ruleSelect);
+								
+								var cDuration = rFlowRule.insertCell(-1);
+								var duration = document.createElement("input");
+								duration.type = "text";
+								duration.setAttribute("class", "durationpick");
+								var rawMinutes = data.settings[setting].duration;
+								var intMinutes = rawMinutes.substring(0, rawMinutes.length-1);
+								var minutes = parseInt(intMinutes);
+								var hours = Math.floor(minutes / 60);
+								minutes = minutes % 60;
+								duration.value = hours + ":" + minutes;
+								cDuration.appendChild(duration);
+
+								var cEmail = rFlowRule.insertCell(-1);
+								var emailCheck = document.createElement("input");
+								emailCheck.type = "checkbox";
+								for(var act in data.settings[setting].actions) {
+									if(data.settings[setting].actions[act] == "EMAIL") {
+										emailCheck.checked = true;
+									}
+								}
+								cEmail.appendChild(emailCheck);
+								
+								var cKill = rFlowRule.insertCell(-1);
+								var killCheck = document.createElement("input");
+								killCheck.type = "checkbox";
+								for(var act in data.settings[setting].actions) {
+									if(data.settings[setting].actions[act] == "KILL") {
+										killCheck.checked = true;
+									}
+								}
+								cKill.appendChild(killCheck);
+								
+								$('.durationpick').timepicker({hourMax: 99});
+							}
+						}
+						$('.durationpick').timepicker({hourMax: 99});
+					}
+				},
+				"json"
+			);
+		
+		$('#slaModalBackground').show();
+		$('#sla-options').show();
+		
+//		this.schedFlowOptions = sched.flowOptions
+		console.log("Loaded schedule info. Ready to set SLA.");
+
+	},
+	handleRemoveSla: function(evt) {
+		console.log("Clicked remove sla button");
+		var scheduleURL = this.scheduleURL;
+		var redirectURL = this.scheduleURL;
+		$.post(
+				scheduleURL,
+				{"action":"removeSla", "projectId":this.projectId, "flowName":this.flowName},
+				function(data) {
+				if (data.error) {
+						$('#errorMsg').text(data.error)
+					}
+					else {
+						window.location = redirectURL
+					}
+				"json"
+				}
+			);
+
+	},
+	handleSetSla: function(evt) {
+
+		var slaEmails = $('#slaEmails').val();
+		var settings = {};
+		
+		
+		var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+		for(var row = 0; row < tFlowRules.rows.length-1; row++) {
+			var rFlowRule = tFlowRules.rows[row];
+			var id = rFlowRule.cells[0].firstChild.value;
+			var rule = rFlowRule.cells[1].firstChild.value;
+			var duration = rFlowRule.cells[2].firstChild.value;
+			var email = rFlowRule.cells[3].firstChild.checked;
+			var kill = rFlowRule.cells[4].firstChild.checked;
+			settings[row] = id + "," + rule + "," + duration + "," + email + "," + kill; 
+		}
+
+		var slaData = {
+			projectId: this.projectId,
+			flowName: this.flowName,
+			ajax: "setSla",			
+			slaEmails: slaEmails,
+			settings: settings
+		};
+
+		var scheduleURL = this.scheduleURL;
+		
+		$.post(
+			scheduleURL,
+			slaData,
+			function(data) {
+				if (data.error) {
+					alert(data.error);
+				}
+				else {
+					tFlowRules.length = 0;
+					window.location = scheduleURL;
+				}
+			},
+			"json"
+		);
+	},
+	handleAddRow: function(evt) {
+		
+		var indexToName = this.indexToName;
+		var nameToIndex = this.nameToIndex;
+		var indexToText = this.indexToText;
+		var ruleBoxOptions = this.ruleBoxOptions;
+
+		var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+		var rFlowRule = tFlowRules.insertRow(tFlowRules.rows.length-1);
+		
+		var cId = rFlowRule.insertCell(-1);
+		var idSelect = document.createElement("select");
+		for(var i in indexToName) {
+			idSelect.options[i] = new Option(indexToText[i], indexToName[i]);
+		}
+		
+		cId.appendChild(idSelect);
+		
+		var cRule = rFlowRule.insertCell(-1);
+		var ruleSelect = document.createElement("select");
+		for(var i in ruleBoxOptions) {
+			ruleSelect.options[i] = new Option(ruleBoxOptions[i], ruleBoxOptions[i]);
+		}
+		cRule.appendChild(ruleSelect);
+		
+		var cDuration = rFlowRule.insertCell(-1);
+		var duration = document.createElement("input");
+		duration.type = "text";
+		duration.setAttribute("class", "durationpick");
+		cDuration.appendChild(duration);
+
+		var cEmail = rFlowRule.insertCell(-1);
+		var emailCheck = document.createElement("input");
+		emailCheck.type = "checkbox";
+		cEmail.appendChild(emailCheck);
+		
+		var cKill = rFlowRule.insertCell(-1);
+		var killCheck = document.createElement("input");
+		killCheck.type = "checkbox";
+		cKill.appendChild(killCheck);
+		
+		$('.durationpick').timepicker({hourMax: 99});
+
+		return rFlowRule;
+	},
+	handleEditColumn : function(evt) {
+		var curTarget = evt.currentTarget;
+	
+		if (this.editingTarget != curTarget) {
+			this.closeEditingTarget();
+			
+			var text = $(curTarget).children(".spanValue").text();
+			$(curTarget).empty();
+						
+			var input = document.createElement("input");
+			$(input).attr("type", "text");
+			$(input).css("width", "100%");
+			$(input).val(text);
+			$(curTarget).addClass("editing");
+			$(curTarget).append(input);
+			$(input).focus();
+			this.editingTarget = curTarget;
+		}
+	},
+	handleRemoveColumn : function(evt) {
+		var curTarget = evt.currentTarget;
+		// Should be the table
+		var row = curTarget.parentElement.parentElement;
+		$(row).remove();
+	},
+	closeEditingTarget: function(evt) {
+
+	}
+});
+
+var slaView;
+var tableSorterView;
+$(function() {
+	var selected;
+
+
+	slaView = new azkaban.ChangeSlaView({el:$('#sla-options')});
+	tableSorterView = new azkaban.TableSorter({el:$('#scheduledFlowsTbl')});
+//	var requestURL = contextURL + "/manager";
+
+	// Set up the Flow options view. Create a new one every time :p
+//	 $('#addSlaBtn').click( function() {
+//		 slaView.show();
+//	 });
+
+	 
+	
+});
\ No newline at end of file
diff --git a/src/web/js/azkaban.schedule.svg.js b/src/web/js/azkaban.schedule.svg.js
new file mode 100644
index 0000000..d272251
--- /dev/null
+++ b/src/web/js/azkaban.schedule.svg.js
@@ -0,0 +1,450 @@
+$.namespace('azkaban');
+
+$(function() {
+
+	var border = 20;
+	var header = 30;
+	var minTimeWidth = 80;
+	var timeWidth = minTimeWidth;
+	var lineHeight = 40;
+	var numDays = 7;
+	var today = new Date();
+	var totalHeight = (border * 2 + header + 24 * lineHeight);
+	var monthConst = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
+	var dayOfWeekConst = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
+	var hourMillisConst = 3600 * 1000;
+	var dayMillisConst = 24 * hourMillisConst;
+
+	$("#svgDivCustom").svg({onLoad:
+		function (svg) {
+
+			var totalWidth = $("#svgDivCustom").width();
+
+			$("#svgDivCustom").find("svg").eq(0).removeAttr("width");
+
+
+			//Outer g
+			var gMain = svg.group({transform: "translate(" + border + ".5," + border + ".5)", stroke : "#999", strokeWidth: 1});
+			var defaultDate = new Date(today.setDate(today.getDate() - today.getDay()));
+			today = new Date();
+			var svgDate = defaultDate;
+
+			//Load the date from the hash if existing
+			if(window.location.hash) {
+				try {
+					var dateParts = window.location.hash.replace("#", "").split("-");
+					var newDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]));
+					if(!isNaN(newDate)) {
+						svgDate = newDate;
+					}
+				}
+				catch(err){ }
+			}
+
+			//Used to filter projects or flows out
+			var filterProject = new Array();
+			var filterFlow = new Array();
+
+			$(".nav-prev-week").click(function (event) {
+				svgDate = new Date(svgDate.valueOf() - 7 * dayMillisConst);
+				window.location.hash = "#" + svgDate.getFullYear() + "-" + (svgDate.getMonth() + 1) + "-" + svgDate.getDate();
+				loadSvg(svgDate);
+				event.stopPropagation();
+			});
+			$(".nav-next-week").click(function (event) {
+				svgDate = new Date(svgDate.valueOf() + 7 * dayMillisConst);
+				window.location.hash = "#" + svgDate.getFullYear() + "-" + (svgDate.getMonth() + 1) + "-" + svgDate.getDate();
+				loadSvg(svgDate);
+				event.stopPropagation();
+			});
+			$(".nav-this-week").click(function (event) {
+				svgDate = defaultDate;
+				window.location.hash = "#" + svgDate.getFullYear() + "-" + (svgDate.getMonth() + 1) + "-" + svgDate.getDate();
+				loadSvg(svgDate);
+				event.stopPropagation();
+			});
+
+
+
+			loadSvg(svgDate);
+
+			function loadSvg(firstDay)
+			{
+				//Text to show which month/year it is
+				var monthIndicatorText = monthConst[firstDay.getMonth()] + " " + firstDay.getFullYear().toString();
+				//Measure a good width for the text to display well
+				timeWidth = Math.max(minTimeWidth, measureText(svg, monthIndicatorText, {fontSize: "20", style: "text-anchor: end;"}));
+
+				var dayWidth = Math.floor((totalWidth - 3 * border - timeWidth) / numDays);
+
+				//svg.configure({viewBox: "0 0 " + totalWidth + " " + totalHeight, style: "width:100%"}, true);
+				svg.remove(gMain);
+				gMain = svg.group({transform: "translate(" + border + ".5," + border + ".5)", stroke : "#999", strokeWidth: 1});
+				svg.text(gMain, timeWidth, header - 8, monthIndicatorText, {fontSize: "20", style: "text-anchor: end;", fill : "#F60", stroke : "none"});
+				//time indicator group
+				var gLeft = svg.group(gMain, {transform: "translate(0," + header + ")"});
+				//Draw lines and hours
+				for(var i = 0; i < 24; i++)
+				{
+					svg.line(gLeft, 0, i * lineHeight, timeWidth, i * lineHeight);
+					//Gets the hour text from an integer from 0 to 23
+					var hourText = getHourText(i);
+					//Move text down a bit? TODO: Is there a CSS option for top anchor?
+					svg.text(gLeft, timeWidth, i * lineHeight + 15, hourText, {fontSize: "14", style: "text-anchor: end;", fill : "#333", stroke : "none"});
+				}
+
+				//var firstDay = new Date();//(new Date()).valueOf();
+				firstDay = new Date(firstDay.getFullYear(), firstDay.getMonth(), firstDay.getDate()).valueOf();
+				var isThisWeek = -1;
+				//Draw background
+				for(var deltaDay = 0; deltaDay < numDays; deltaDay++)
+				{
+					//Day group
+					var gDay = svg.group(gMain, {transform: "translate(" + (border + timeWidth + deltaDay * dayWidth) + "," + header + ")"});
+
+					//This is temporary.
+					var date = new Date(firstDay + dayMillisConst * deltaDay);
+					var day = date.getDate();
+
+					//Draw box around
+					var isToday = date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() == today.getDate();
+					if(isToday)
+					{
+						isThisWeek = deltaDay;
+					}
+					svg.rect(gDay, 0, -header, dayWidth, 24 * lineHeight + header, {fill : "none", stroke : "#F60"});
+					//Draw day title
+					svg.text(gDay, 6, -8, day + " " + dayOfWeekConst[date.getDay()], {fontSize: "20", fill : isToday?"#06C":"#F60", stroke : "none"});
+
+					//Draw horizontal lines
+					for(var i = 0; i < 24; i++)
+					{
+						svg.line(gDay, 0, i * lineHeight, dayWidth, i * lineHeight);
+					}
+				}
+
+				if(isThisWeek != -1)
+				{
+					var date = new Date(firstDay + dayMillisConst * isThisWeek);
+					var day = date.getDate();
+					var gDay = svg.group(gMain, {transform: "translate(" + (border + timeWidth + isThisWeek * dayWidth) + "," + header + ")"});
+					svg.rect(gDay, 0, -header, dayWidth, 24 * lineHeight + header, {fill : "none", stroke : "#06F"});
+				}
+
+				var gDayView = svg.group(gMain, {transform: "translate(" + (border + timeWidth) + "," + header + ")"});
+				//A list of all items
+				var itemByDay = new Array();
+				for(var deltaDay = 0; deltaDay < numDays; deltaDay++) {
+					itemByDay[deltaDay] = new Array();
+				}
+
+				function filterApplies(item) {
+					for(var i = 0; i < filterProject.length; i++) {
+						if(item.projectname == filterProject[i].projectname) {
+							return true;
+						}
+					}
+					for(var i = 0; i < filterFlow.length; i++) {
+						if(item.projectname == filterFlow[i].projectname && item.flowname == filterFlow[i].flowname) {
+							return true;
+						}
+					}
+					return false;
+				}
+
+				//Function that re-renders all loaded items
+				function renderDays() {
+					//Clear items inside the day view
+					svg.remove(gDayView);
+					gDayView = svg.group(gMain, {transform: "translate(" + (border + timeWidth) + "," + header + ")"});
+
+					//Add day groups
+					for(var deltaDay = 0; deltaDay < numDays; deltaDay++) {
+						var gDay = svg.group(gDayView, {transform: "translate(" + (deltaDay * dayWidth) + ")"});
+						var data = itemByDay[deltaDay];
+						//Sort the arrays to have a better view
+						data.sort(function (a, b){
+							//Smaller time in front
+							var timeDiff = a.time - b.time;
+							if(timeDiff == 0) {
+								//Larger length in front
+								var lengthDiff = b.length - a.length;
+								if(lengthDiff == 0) {
+									//Sort by alphabetical
+									return (a.flowname < b.flowname ? 1 : a.flowname > b.flowname ? -1 : 0);
+								}
+								return lengthDiff;
+							}
+							return timeDiff;
+						});
+						//Sort items to columns
+						var columns = new Array();
+						columns.push(new Array());
+						//Every item is parsed through here into columns
+						for(var i = 0; i < data.length; i++) {
+							//Apply filters here
+							if(filterApplies(data[i])) {
+								continue;
+							}
+
+							var foundColumn = false;
+							//Go through every column until a place can be found
+							for(var j = 0; j < columns.length; j++) {
+								if(!intersectArray(data[i], columns[j])) {
+									//Found a place
+									columns[j].push(data[i]);
+									foundColumn = true;
+									break;
+								}
+							}
+							//No place, create new column
+							if(!foundColumn) {
+								columns.push(new Array());
+								columns[columns.length - 1].push(data[i]);
+							}
+						}
+
+						//Actually drawing them
+						for(var i = 0; i < columns.length; i++) {
+							//Split into columns
+							var gColumn = svg.group(gDay, {transform: "translate(" + Math.floor(i * dayWidth / columns.length) + ")", style: "opacity: 0.8"});
+							for(var j = 0; j < columns[i].length; j++) {
+								//Draw item
+								var item = columns[i][j];
+								var startTime = new Date(item.time);
+								var startY = Math.floor(startTime.getHours() * lineHeight + startTime.getMinutes() * lineHeight / 60);
+								var endTime = new Date(item.time + item.length );
+								var endY = Math.floor(startY + (item.length * lineHeight) / hourMillisConst);
+								//var anchor = svg.a(gColumn);
+								var itemUrl = contextURL + "/manager?project=" + item.projectname + "&flow=" + item.flowname;
+								var gItem = svg.link(gColumn, itemUrl, {transform: "translate(0," + startY + ")"});
+
+								//Pass the item into the DOM data store to be retrieved later on
+								$(gItem).data("item", item);
+
+								//Replace the context handler
+								gItem.addEventListener('contextmenu', handleContextMenu);
+
+								//Add a tooltip on mouse over
+								gItem.addEventListener('mouseover', handleMouseOver);
+								//Remove the tooltip on mouse out
+								gItem.addEventListener('mouseout', handleMouseOut);
+
+								//$(gItem).attr("style","color:red");
+								var rect = svg.rect(gItem, 0, 0, Math.ceil(dayWidth / columns.length), Math.floor(endY - startY), 0, 0, {fill : "#7E7", stroke : "#444", strokeWidth : 1});
+								
+								item.rect = rect;
+								//Draw text
+								//svg.text(gItem, 6, 16, item.flowname, {fontSize: "13", fill : "#000", stroke : "none"});
+							}
+						}
+					}
+				}
+
+				function processItem(item)
+				{
+					var firstTime = item.time;
+					var startTime = firstDay;
+					var endTime = firstDay + numDays * dayMillisConst;
+					var period = item.period;
+
+					// Shift time until we're past the start time
+					if (period > 0) {
+						// Calculate next execution time efficiently
+						// Take into account items that ends in the date specified, but does not start on that date
+						var periods = Math.floor((startTime - (firstTime + item.length)) / period);
+						//Make sure we don't subtract
+						if(periods < 0){
+							periods = 0;
+						}
+						firstTime += period * periods;
+						// Increment in case we haven't arrived yet. This will apply to most of the cases
+						while (firstTime + item.length < startTime) {
+							firstTime += period;
+						}
+					}
+
+					// Bad or no period
+					if (period <= 0) {
+						// Single instance case
+						if (firstTime >= startTime && firstTime < endTime) {
+							addItem({flowname : item.flowname, projectname: item.projectname, time: firstTime, length: item.length, item: item});
+						}
+					}
+					else {
+						if(period <= hourMillisConst) {
+							addItem({flowname : item.flowname, projectname: item.projectname, time: firstTime, length: endTime - firstTime, item: item});
+						}
+						else{
+							// Repetitive schedule, firstTime is assumed to be after startTime
+							while (firstTime < endTime) {
+								addItem({flowname : item.flowname, projectname: item.projectname, time: firstTime, length: item.length, item: item});
+								firstTime += period;
+							}
+						}
+					}
+				}
+
+				function addItem(obj)
+				{
+					var itemStartTime = new Date(obj.time);
+					var itemEndTime = new Date(obj.time + obj.length);
+					var itemStartDate = new Date(itemStartTime.getFullYear(), itemStartTime.getMonth(), itemStartTime.getDate());
+					var itemEndDate = new Date(itemEndTime.getFullYear(), itemEndTime.getMonth(), itemEndTime.getDate());
+
+					//Cross date item, cut it to only today's portion and add another item starting tomorrow morning
+					if(itemStartDate.valueOf() != itemEndDate.valueOf() && itemEndTime.valueOf() != itemStartDate + dayMillisConst)
+					{
+						var nextMorning = itemStartDate.valueOf() + dayMillisConst;
+						var excess = obj.length - (nextMorning - itemStartTime.valueOf());
+						obj.length = nextMorning - itemStartTime.valueOf();
+						while(excess > 0)
+						{
+							var tempLength = excess;
+							if(tempLength > dayMillisConst){
+								tempLength = dayMillisConst;
+							}
+
+							var item2 = {time: nextMorning, length: tempLength, projectname: obj.projectname, flowname: obj.flowname, item: obj.item};
+							addItem(item2);
+							excess -= tempLength;
+							nextMorning += dayMillisConst;
+						}
+					}
+
+					//Now the item should be only in one day
+					var index = (itemStartDate.valueOf() - firstDay) / dayMillisConst;
+					if(index >= 0 && index < numDays)
+					{
+						//Add the item to the rendering list
+						itemByDay[index].push(obj);
+						obj.item.objs.push(obj);
+					}
+				}
+
+				function handleContextMenu(event) {
+					var requestURL = $(this).attr("href");
+					var item = $(this).data("item");
+					var menu = [
+						{title: "Job \"" + item.flowname + "\" From Project \"" + item.projectname + "\""},
+						{title: "View Job", callback: function() {window.location.href=requestURL;}},
+						{title: "View Job in New Window", callback: function() {window.open(requestURL);}},
+						{title: "Hide Job", callback: function() {filterFlow.push(item); renderDays();}},
+						{title: "Hide All Jobs From the Same Project", callback: function() {filterProject.push(item); renderDays();}}
+					];
+					contextMenuView.show(event, menu);
+					event.preventDefault();
+					event.stopPropagation();
+					return false;
+				}
+
+				function handleMouseOver(event) {
+					//Create the new tooltip
+					var requestURL = $(this).attr("href");
+					var obj = $(this).data("item");
+					var offset = $("svg").offset();
+					var thisOffset = $(this).offset();
+
+					var tooltip = svg.group({transform: "translate(" + (thisOffset.left - offset.left + 2) + "," + (thisOffset.top - offset.top - 2) + ")"});
+					var text = [
+						"\"" + obj.flowname + "\" from \"" + obj.projectname + "\"",
+						"Repeat: " + formatReadablePeriod(obj.item.period)
+					];
+					var textLength = Math.max(measureText(svg, text[0], {fontSize: "13"}), measureText(svg, text[1], {fontSize: "13"}));
+					var rect = svg.rect(tooltip, 0, -40, textLength + 4, 40, {fill : "#FFF", stroke : "none"});
+					svg.text(tooltip, 2, -25, text[0], {fontSize: "13", fill : "#000", stroke : "none"});
+					svg.text(tooltip, 2, -5, text[1], {fontSize: "13", fill : "#000", stroke : "none"});
+
+					//Item highlight effect
+					for(var i = 0; i < obj.item.objs.length; i++) {
+						var obj2 = obj.item.objs[i];
+						$(obj2.rect).attr("fill", "#F00");
+					}
+
+					//Store tooltip
+					$(this).data("tooltip", tooltip);
+				}
+
+				function handleMouseOut(event) {
+					//Item highlight effect
+					var obj = $(this).data("item");
+					for(var i = 0; i < obj.item.objs.length; i++) {
+						var obj2 = obj.item.objs[i];
+						$(obj2.rect).attr("fill", "#7E7");
+					}
+					//Clear the fade interval
+					$($(this).data("tooltip")).fadeOut(250, function(){ svg.remove(this); });
+				}
+
+				//Asynchronously load data
+				var requestURL = contextURL + "/schedule";
+				$.ajax({
+					type: "GET",
+					url: requestURL,
+					data: {"ajax": "loadFlow"},
+					dataType: "json",
+					success: function (data)
+					{
+						var items = data.items;
+
+						//Sort items by day
+						for(var i = 0; i < items.length; i++)
+						{
+							items[i].length = hourMillisConst; //TODO: parseInt(items[i].length);
+							items[i].time = parseInt(items[i].time);
+							items[i].period = parseInt(items[i].period);
+							items[i].objs = new Array();
+							processItem(items[i]);
+						}
+						//Trigger a re-rendering of all the data
+						renderDays();
+					}
+				});
+			}
+		}, settings : {
+			"xmlns" : "http://www.w3.org/2000/svg", 
+			"xmlns:xlink" : "http://www.w3.org/1999/xlink", 
+			"shape-rendering" : "optimize-speed",
+			"style" : "width:100%;height:" + totalHeight + "px"
+		}});
+
+	function dayMatch(d1, d2) {
+		return d1.getDate() == d2.getDate() && d1.getFullYear() == d2.getFullYear() && d1.getMonth() == d2.getMonth();
+	}
+
+	function getHourText(hour) {
+		return (hour==0 ? "12 AM" : (hour<12 ? hour + " AM" : (hour==12 ? "12 PM" : (hour-12) + " PM" )));
+	}
+
+	function intersectArray(a, arry) {
+		for(var i = 0; i < arry.length; i++) {
+			var b = arry[i];
+			if(a.time < b.time + b.length && a.time + a.length > b.time) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	function measureText(svg, text, options) {
+		var test = svg.text(0, 0, text, options);
+		var width = test.getComputedTextLength();
+		svg.remove(test);
+		return width;
+	}
+
+	function formatReadablePeriod(period) {
+		var days = Math.floor(period / dayMillisConst);
+		var hour = period - days * dayMillisConst;
+		var hours = Math.floor(hour / hourMillisConst);
+		var min = hour - hours * hourMillisConst;
+		var mins = Math.floor(min / 60000);
+
+		var text = "";
+		if(days > 0) text = (days == 1 ? "24 hours" : days.toString() + " days");
+		if(hours > 0) text = text + " " + (hours == 1 ? "1 hour" : hours.toString() + " hours");
+		if(mins > 0) text = text + " " + (mins == 1 ? "1 minute" : mins.toString() + " minutes");
+		return text;
+	}
+});
\ No newline at end of file
diff --git a/src/web/js/azkaban.scheduled.view.js b/src/web/js/azkaban.scheduled.view.js
index a6f1e83..f507dc1 100644
--- a/src/web/js/azkaban.scheduled.view.js
+++ b/src/web/js/azkaban.scheduled.view.js
@@ -1,12 +1,12 @@
 $.namespace('azkaban');
 
 
-function removeSched(projectId, flowName) {
+function removeSched(scheduleId) {
 	var scheduleURL = contextURL + "/schedule"
 	var redirectURL = contextURL + "/schedule"
 	$.post(
 			scheduleURL,
-			{"action":"removeSched", "projectId":projectId, "flowName":flowName},
+			{"action":"removeSched", "scheduleId":scheduleId},
 			function(data) {
 				if (data.error) {
 //                 alert(data.error)
@@ -21,12 +21,12 @@ function removeSched(projectId, flowName) {
 	)
 }
 
-function removeSla(projectId, flowName) {
+function removeSla(scheduleId) {
 	var scheduleURL = contextURL + "/schedule"
 	var redirectURL = contextURL + "/schedule"
 	$.post(
 			scheduleURL,
-			{"action":"removeSla", "projectId":projectId, "flowName":flowName},
+			{"action":"removeSla", "scheduleId":scheduleId},
 			function(data) {
 				if (data.error) {
 //                 alert(data.error)
@@ -68,9 +68,8 @@ azkaban.ChangeSlaView = Backbone.View.extend({
 		}
 		
 	},
-	initFromSched: function(projId, flowName) {
-		this.projectId = projId;
-		this.flowName = flowName;
+	initFromSched: function(scheduleId, flowName) {
+		this.scheduleId = scheduleId;
 		
 		var scheduleURL = contextURL + "/schedule"
 		this.scheduleURL = scheduleURL;
@@ -83,7 +82,7 @@ azkaban.ChangeSlaView = Backbone.View.extend({
 		var ruleBoxOptions = ["SUCCESS", "FINISH"];
 		this.ruleBoxOptions = ruleBoxOptions;
 		
-		var fetchScheduleData = {"projId": this.projectId, "ajax":"slaInfo", "flowName":this.flowName};
+		var fetchScheduleData = {"scheduleId": this.scheduleId, "ajax":"slaInfo"};
 		
 		$.get(
 				this.scheduleURL,
@@ -194,7 +193,7 @@ azkaban.ChangeSlaView = Backbone.View.extend({
 		var redirectURL = this.scheduleURL;
 		$.post(
 				scheduleURL,
-				{"action":"removeSla", "projectId":this.projectId, "flowName":this.flowName},
+				{"action":"removeSla", "scheduleId":this.scheduleId},
 				function(data) {
 				if (data.error) {
 						$('#errorMsg').text(data.error)
@@ -225,8 +224,7 @@ azkaban.ChangeSlaView = Backbone.View.extend({
 		}
 
 		var slaData = {
-			projectId: this.projectId,
-			flowName: this.flowName,
+			scheduleId: this.scheduleId,
 			ajax: "setSla",			
 			slaEmails: slaEmails,
 			settings: settings
diff --git a/src/web/js/jquery/jquery.svg.min.js b/src/web/js/jquery/jquery.svg.min.js
new file mode 100644
index 0000000..5b922fb
--- /dev/null
+++ b/src/web/js/jquery/jquery.svg.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+   SVG for jQuery v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+(function($){function SVGManager(){this._settings=[];this._extensions=[];this.regional=[];this.regional['']={errorLoadingText:'Error loading',notSupportedText:'This browser does not support SVG'};this.local=this.regional[''];this._uuid=new Date().getTime();this._renesis=detectActiveX('RenesisX.RenesisCtrl')}function detectActiveX(a){try{return!!(window.ActiveXObject&&new ActiveXObject(a))}catch(e){return false}}var q='svgwrapper';$.extend(SVGManager.prototype,{markerClassName:'hasSVG',svgNS:'http://www.w3.org/2000/svg',xlinkNS:'http://www.w3.org/1999/xlink',_wrapperClass:SVGWrapper,_attrNames:{class_:'class',in_:'in',alignmentBaseline:'alignment-baseline',baselineShift:'baseline-shift',clipPath:'clip-path',clipRule:'clip-rule',colorInterpolation:'color-interpolation',colorInterpolationFilters:'color-interpolation-filters',colorRendering:'color-rendering',dominantBaseline:'dominant-baseline',enableBackground:'enable-background',fillOpacity:'fill-opacity',fillRule:'fill-rule',floodColor:'flood-color',floodOpacity:'flood-opacity',fontFamily:'font-family',fontSize:'font-size',fontSizeAdjust:'font-size-adjust',fontStretch:'font-stretch',fontStyle:'font-style',fontVariant:'font-variant',fontWeight:'font-weight',glyphOrientationHorizontal:'glyph-orientation-horizontal',glyphOrientationVertical:'glyph-orientation-vertical',horizAdvX:'horiz-adv-x',horizOriginX:'horiz-origin-x',imageRendering:'image-rendering',letterSpacing:'letter-spacing',lightingColor:'lighting-color',markerEnd:'marker-end',markerMid:'marker-mid',markerStart:'marker-start',stopColor:'stop-color',stopOpacity:'stop-opacity',strikethroughPosition:'strikethrough-position',strikethroughThickness:'strikethrough-thickness',strokeDashArray:'stroke-dasharray',strokeDashOffset:'stroke-dashoffset',strokeLineCap:'stroke-linecap',strokeLineJoin:'stroke-linejoin',strokeMiterLimit:'stroke-miterlimit',strokeOpacity:'stroke-opacity',strokeWidth:'stroke-width',textAnchor:'text-anchor',textDecoration:'text-decoration',textRendering:'text-rendering',underlinePosition:'underline-position',underlineThickness:'underline-thickness',vertAdvY:'vert-adv-y',vertOriginY:'vert-origin-y',wordSpacing:'word-spacing',writingMode:'writing-mode'},_attachSVG:function(a,b){var c=(a.namespaceURI==this.svgNS?a:null);var a=(c?null:a);if($(a||c).hasClass(this.markerClassName)){return}if(typeof b=='string'){b={loadURL:b}}else if(typeof b=='function'){b={onLoad:b}}$(a||c).addClass(this.markerClassName);try{if(!c){c=document.createElementNS(this.svgNS,'svg');c.setAttribute('version','1.1');if(a.clientWidth>0){c.setAttribute('width',a.clientWidth)}if(a.clientHeight>0){c.setAttribute('height',a.clientHeight)}a.appendChild(c)}this._afterLoad(a,c,b||{})}catch(e){if($.browser.msie){if(!a.id){a.id='svg'+(this._uuid++)}this._settings[a.id]=b;a.innerHTML='<embed type="image/svg+xml" width="100%" '+'height="100%" src="'+(b.initPath||'')+'blank.svg" '+'pluginspage="http://www.adobe.com/svg/viewer/install/main.html"/>'}else{a.innerHTML='<p class="svg_error">'+this.local.notSupportedText+'</p>'}}},_registerSVG:function(){for(var i=0;i<document.embeds.length;i++){var a=document.embeds[i].parentNode;if(!$(a).hasClass($.svg.markerClassName)||$.data(a,q)){continue}var b=null;try{b=document.embeds[i].getSVGDocument()}catch(e){setTimeout($.svg._registerSVG,250);return}b=(b?b.documentElement:null);if(b){$.svg._afterLoad(a,b)}}},_afterLoad:function(a,b,c){var c=c||this._settings[a.id];this._settings[a?a.id:'']=null;var d=new this._wrapperClass(b,a);$.data(a||b,q,d);try{if(c.loadURL){d.load(c.loadURL,c)}if(c.settings){d.configure(c.settings)}if(c.onLoad&&!c.loadURL){c.onLoad.apply(a||b,[d])}}catch(e){alert(e)}},_getSVG:function(a){a=(typeof a=='string'?$(a)[0]:(a.jquery?a[0]:a));return $.data(a,q)},_destroySVG:function(a){var b=$(a);if(!b.hasClass(this.markerClassName)){return}b.removeClass(this.markerClassName);if(a.namespaceURI!=this.svgNS){b.empty()}$.removeData(a,q)},addExtension:function(a,b){this._extensions.push([a,b])},isSVGElem:function(a){return(a.nodeType==1&&a.namespaceURI==$.svg.svgNS)}});function SVGWrapper(a,b){this._svg=a;this._container=b;for(var i=0;i<$.svg._extensions.length;i++){var c=$.svg._extensions[i];this[c[0]]=new c[1](this)}}$.extend(SVGWrapper.prototype,{_width:function(){return(this._container?this._container.clientWidth:this._svg.width)},_height:function(){return(this._container?this._container.clientHeight:this._svg.height)},root:function(){return this._svg},configure:function(a,b,c){if(!a.nodeName){c=b;b=a;a=this._svg}if(c){for(var i=a.attributes.length-1;i>=0;i--){var d=a.attributes.item(i);if(!(d.nodeName=='onload'||d.nodeName=='version'||d.nodeName.substring(0,5)=='xmlns')){a.attributes.removeNamedItem(d.nodeName)}}}for(var e in b){a.setAttribute($.svg._attrNames[e]||e,b[e])}return this},getElementById:function(a){return this._svg.ownerDocument.getElementById(a)},change:function(a,b){if(a){for(var c in b){if(b[c]==null){a.removeAttribute($.svg._attrNames[c]||c)}else{a.setAttribute($.svg._attrNames[c]||c,b[c])}}}return this},_args:function(b,c,d){c.splice(0,0,'parent');c.splice(c.length,0,'settings');var e={};var f=0;if(b[0]!=null&&b[0].jquery){b[0]=b[0][0]}if(b[0]!=null&&!(typeof b[0]=='object'&&b[0].nodeName)){e['parent']=null;f=1}for(var i=0;i<b.length;i++){e[c[i+f]]=b[i]}if(d){$.each(d,function(i,a){if(typeof e[a]=='object'){e.settings=e[a];e[a]=null}})}return e},title:function(a,b,c){var d=this._args(arguments,['text']);var e=this._makeNode(d.parent,'title',d.settings||{});e.appendChild(this._svg.ownerDocument.createTextNode(d.text));return e},describe:function(a,b,c){var d=this._args(arguments,['text']);var e=this._makeNode(d.parent,'desc',d.settings||{});e.appendChild(this._svg.ownerDocument.createTextNode(d.text));return e},defs:function(a,b,c){var d=this._args(arguments,['id'],['id']);return this._makeNode(d.parent,'defs',$.extend((d.id?{id:d.id}:{}),d.settings||{}))},symbol:function(a,b,c,d,e,f,g){var h=this._args(arguments,['id','x1','y1','width','height']);return this._makeNode(h.parent,'symbol',$.extend({id:h.id,viewBox:h.x1+' '+h.y1+' '+h.width+' '+h.height},h.settings||{}))},marker:function(a,b,c,d,e,f,g,h){var i=this._args(arguments,['id','refX','refY','mWidth','mHeight','orient'],['orient']);return this._makeNode(i.parent,'marker',$.extend({id:i.id,refX:i.refX,refY:i.refY,markerWidth:i.mWidth,markerHeight:i.mHeight,orient:i.orient||'auto'},i.settings||{}))},style:function(a,b,c){var d=this._args(arguments,['styles']);var e=this._makeNode(d.parent,'style',$.extend({type:'text/css'},d.settings||{}));e.appendChild(this._svg.ownerDocument.createTextNode(d.styles));if($.browser.opera){$('head').append('<style type="text/css">'+d.styles+'</style>')}return e},script:function(a,b,c,d){var e=this._args(arguments,['script','type'],['type']);var f=this._makeNode(e.parent,'script',$.extend({type:e.type||'text/javascript'},e.settings||{}));f.appendChild(this._svg.ownerDocument.createTextNode(e.script));if(!$.browser.mozilla){$.globalEval(e.script)}return f},linearGradient:function(a,b,c,d,e,f,g,h){var i=this._args(arguments,['id','stops','x1','y1','x2','y2'],['x1']);var j=$.extend({id:i.id},(i.x1!=null?{x1:i.x1,y1:i.y1,x2:i.x2,y2:i.y2}:{}));return this._gradient(i.parent,'linearGradient',$.extend(j,i.settings||{}),i.stops)},radialGradient:function(a,b,c,d,e,r,f,g,h){var i=this._args(arguments,['id','stops','cx','cy','r','fx','fy'],['cx']);var j=$.extend({id:i.id},(i.cx!=null?{cx:i.cx,cy:i.cy,r:i.r,fx:i.fx,fy:i.fy}:{}));return this._gradient(i.parent,'radialGradient',$.extend(j,i.settings||{}),i.stops)},_gradient:function(a,b,c,d){var e=this._makeNode(a,b,c);for(var i=0;i<d.length;i++){var f=d[i];this._makeNode(e,'stop',$.extend({offset:f[0],stopColor:f[1]},(f[2]!=null?{stopOpacity:f[2]}:{})))}return e},pattern:function(a,b,x,y,c,d,e,f,g,h,i){var j=this._args(arguments,['id','x','y','width','height','vx','vy','vwidth','vheight'],['vx']);var k=$.extend({id:j.id,x:j.x,y:j.y,width:j.width,height:j.height},(j.vx!=null?{viewBox:j.vx+' '+j.vy+' '+j.vwidth+' '+j.vheight}:{}));return this._makeNode(j.parent,'pattern',$.extend(k,j.settings||{}))},clipPath:function(a,b,c,d){var e=this._args(arguments,['id','units']);e.units=e.units||'userSpaceOnUse';return this._makeNode(e.parent,'clipPath',$.extend({id:e.id,clipPathUnits:e.units},e.settings||{}))},mask:function(a,b,x,y,c,d,e){var f=this._args(arguments,['id','x','y','width','height']);return this._makeNode(f.parent,'mask',$.extend({id:f.id,x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}))},createPath:function(){return new SVGPath()},createText:function(){return new SVGText()},svg:function(a,x,y,b,c,d,e,f,g,h){var i=this._args(arguments,['x','y','width','height','vx','vy','vwidth','vheight'],['vx']);var j=$.extend({x:i.x,y:i.y,width:i.width,height:i.height},(i.vx!=null?{viewBox:i.vx+' '+i.vy+' '+i.vwidth+' '+i.vheight}:{}));return this._makeNode(i.parent,'svg',$.extend(j,i.settings||{}))},group:function(a,b,c){var d=this._args(arguments,['id'],['id']);return this._makeNode(d.parent,'g',$.extend({id:d.id},d.settings||{}))},use:function(a,x,y,b,c,d,e){var f=this._args(arguments,['x','y','width','height','ref']);if(typeof f.x=='string'){f.ref=f.x;f.settings=f.y;f.x=f.y=f.width=f.height=null}var g=this._makeNode(f.parent,'use',$.extend({x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}));g.setAttributeNS($.svg.xlinkNS,'href',f.ref);return g},link:function(a,b,c){var d=this._args(arguments,['ref']);var e=this._makeNode(d.parent,'a',d.settings);e.setAttributeNS($.svg.xlinkNS,'href',d.ref);return e},image:function(a,x,y,b,c,d,e){var f=this._args(arguments,['x','y','width','height','ref']);var g=this._makeNode(f.parent,'image',$.extend({x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}));g.setAttributeNS($.svg.xlinkNS,'href',f.ref);return g},path:function(a,b,c){var d=this._args(arguments,['path']);return this._makeNode(d.parent,'path',$.extend({d:(d.path.path?d.path.path():d.path)},d.settings||{}))},rect:function(a,x,y,b,c,d,e,f){var g=this._args(arguments,['x','y','width','height','rx','ry'],['rx']);return this._makeNode(g.parent,'rect',$.extend({x:g.x,y:g.y,width:g.width,height:g.height},(g.rx?{rx:g.rx,ry:g.ry}:{}),g.settings||{}))},circle:function(a,b,c,r,d){var e=this._args(arguments,['cx','cy','r']);return this._makeNode(e.parent,'circle',$.extend({cx:e.cx,cy:e.cy,r:e.r},e.settings||{}))},ellipse:function(a,b,c,d,e,f){var g=this._args(arguments,['cx','cy','rx','ry']);return this._makeNode(g.parent,'ellipse',$.extend({cx:g.cx,cy:g.cy,rx:g.rx,ry:g.ry},g.settings||{}))},line:function(a,b,c,d,e,f){var g=this._args(arguments,['x1','y1','x2','y2']);return this._makeNode(g.parent,'line',$.extend({x1:g.x1,y1:g.y1,x2:g.x2,y2:g.y2},g.settings||{}))},polyline:function(a,b,c){var d=this._args(arguments,['points']);return this._poly(d.parent,'polyline',d.points,d.settings)},polygon:function(a,b,c){var d=this._args(arguments,['points']);return this._poly(d.parent,'polygon',d.points,d.settings)},_poly:function(a,b,c,d){var e='';for(var i=0;i<c.length;i++){e+=c[i].join()+' '}return this._makeNode(a,b,$.extend({points:$.trim(e)},d||{}))},text:function(a,x,y,b,c){var d=this._args(arguments,['x','y','value']);if(typeof d.x=='string'&&arguments.length<4){d.value=d.x;d.settings=d.y;d.x=d.y=null}return this._text(d.parent,'text',d.value,$.extend({x:(d.x&&isArray(d.x)?d.x.join(' '):d.x),y:(d.y&&isArray(d.y)?d.y.join(' '):d.y)},d.settings||{}))},textpath:function(a,b,c,d){var e=this._args(arguments,['path','value']);var f=this._text(e.parent,'textPath',e.value,e.settings||{});f.setAttributeNS($.svg.xlinkNS,'href',e.path);return f},_text:function(a,b,c,d){var e=this._makeNode(a,b,d);if(typeof c=='string'){e.appendChild(e.ownerDocument.createTextNode(c))}else{for(var i=0;i<c._parts.length;i++){var f=c._parts[i];if(f[0]=='tspan'){var g=this._makeNode(e,f[0],f[2]);g.appendChild(e.ownerDocument.createTextNode(f[1]));e.appendChild(g)}else if(f[0]=='tref'){var g=this._makeNode(e,f[0],f[2]);g.setAttributeNS($.svg.xlinkNS,'href',f[1]);e.appendChild(g)}else if(f[0]=='textpath'){var h=$.extend({},f[2]);h.href=null;var g=this._makeNode(e,f[0],h);g.setAttributeNS($.svg.xlinkNS,'href',f[2].href);g.appendChild(e.ownerDocument.createTextNode(f[1]));e.appendChild(g)}else{e.appendChild(e.ownerDocument.createTextNode(f[1]))}}}return e},other:function(a,b,c){var d=this._args(arguments,['name']);return this._makeNode(d.parent,d.name,d.settings||{})},_makeNode:function(a,b,c){a=a||this._svg;var d=this._svg.ownerDocument.createElementNS($.svg.svgNS,b);for(var b in c){var e=c[b];if(e!=null&&e!=null&&(typeof e!='string'||e!='')){d.setAttribute($.svg._attrNames[b]||b,e)}}a.appendChild(d);return d},add:function(b,c){var d=this._args((arguments.length==1?[null,b]:arguments),['node']);var f=this;d.parent=d.parent||this._svg;d.node=(d.node.jquery?d.node:$(d.node));try{if($.svg._renesis){throw'Force traversal';}d.parent.appendChild(d.node.cloneNode(true))}catch(e){d.node.each(function(){var a=f._cloneAsSVG(this);if(a){d.parent.appendChild(a)}})}return this},clone:function(b,c){var d=this;var e=this._args((arguments.length==1?[null,b]:arguments),['node']);e.parent=e.parent||this._svg;e.node=(e.node.jquery?e.node:$(e.node));var f=[];e.node.each(function(){var a=d._cloneAsSVG(this);if(a){a.id='';e.parent.appendChild(a);f.push(a)}});return f},_cloneAsSVG:function(a){var b=null;if(a.nodeType==1){b=this._svg.ownerDocument.createElementNS($.svg.svgNS,this._checkName(a.nodeName));for(var i=0;i<a.attributes.length;i++){var c=a.attributes.item(i);if(c.nodeName!='xmlns'&&c.nodeValue){if(c.prefix=='xlink'){b.setAttributeNS($.svg.xlinkNS,c.localName||c.baseName,c.nodeValue)}else{b.setAttribute(this._checkName(c.nodeName),c.nodeValue)}}}for(var i=0;i<a.childNodes.length;i++){var d=this._cloneAsSVG(a.childNodes[i]);if(d){b.appendChild(d)}}}else if(a.nodeType==3){if($.trim(a.nodeValue)){b=this._svg.ownerDocument.createTextNode(a.nodeValue)}}else if(a.nodeType==4){if($.trim(a.nodeValue)){try{b=this._svg.ownerDocument.createCDATASection(a.nodeValue)}catch(e){b=this._svg.ownerDocument.createTextNode(a.nodeValue.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'))}}}return b},_checkName:function(a){a=(a.substring(0,1)>='A'&&a.substring(0,1)<='Z'?a.toLowerCase():a);return(a.substring(0,4)=='svg:'?a.substring(4):a)},load:function(j,k){k=(typeof k=='boolean'?{addTo:k}:(typeof k=='function'?{onLoad:k}:(typeof k=='string'?{parent:k}:(typeof k=='object'&&k.nodeName?{parent:k}:(typeof k=='object'&&k.jquery?{parent:k}:k||{})))));if(!k.parent&&!k.addTo){this.clear(false)}var l=[this._svg.getAttribute('width'),this._svg.getAttribute('height')];var m=this;var n=function(a){a=$.svg.local.errorLoadingText+': '+a;if(k.onLoad){k.onLoad.apply(m._container||m._svg,[m,a])}else{m.text(null,10,20,a)}};var o=function(a){var b=new ActiveXObject('Microsoft.XMLDOM');b.validateOnParse=false;b.resolveExternals=false;b.async=false;b.loadXML(a);if(b.parseError.errorCode!=0){n(b.parseError.reason);return null}return b};var p=function(a){if(!a){return}if(a.documentElement.nodeName!='svg'){var b=a.getElementsByTagName('parsererror');var c=(b.length?b[0].getElementsByTagName('div'):[]);n(!b.length?'???':(c.length?c[0]:b[0]).firstChild.nodeValue);return}var d=(k.parent?$(k.parent)[0]:m._svg);var f={};for(var i=0;i<a.documentElement.attributes.length;i++){var g=a.documentElement.attributes.item(i);if(!(g.nodeName=='version'||g.nodeName.substring(0,5)=='xmlns')){f[g.nodeName]=g.nodeValue}}m.configure(d,f,!k.parent);var h=a.documentElement.childNodes;for(var i=0;i<h.length;i++){try{if($.svg._renesis){throw'Force traversal';}d.appendChild(m._svg.ownerDocument.importNode(h[i],true));if(h[i].nodeName=='script'){$.globalEval(h[i].textContent)}}catch(e){m.add(d,h[i])}}if(!k.changeSize){m.configure(d,{width:l[0],height:l[1]})}if(k.onLoad){k.onLoad.apply(m._container||m._svg,[m])}};if(j.match('<svg')){p($.browser.msie?o(j):new DOMParser().parseFromString(j,'text/xml'))}else{$.ajax({url:j,dataType:($.browser.msie?'text':'xml'),success:function(a){p($.browser.msie?o(a):a)},error:function(a,b,c){n(b+(c?' '+c.message:''))}})}return this},remove:function(a){a=(a.jquery?a[0]:a);a.parentNode.removeChild(a);return this},clear:function(a){if(a){this.configure({},true)}while(this._svg.firstChild){this._svg.removeChild(this._svg.firstChild)}return this},toSVG:function(a){a=a||this._svg;return(typeof XMLSerializer=='undefined'?this._toSVG(a):new XMLSerializer().serializeToString(a))},_toSVG:function(a){var b='';if(!a){return b}if(a.nodeType==3){b=a.nodeValue}else if(a.nodeType==4){b='<![CDATA['+a.nodeValue+']]>'}else{b='<'+a.nodeName;if(a.attributes){for(var i=0;i<a.attributes.length;i++){var c=a.attributes.item(i);if(!($.trim(c.nodeValue)==''||c.nodeValue.match(/^\[object/)||c.nodeValue.match(/^function/))){b+=' '+(c.namespaceURI==$.svg.xlinkNS?'xlink:':'')+c.nodeName+'="'+c.nodeValue+'"'}}}if(a.firstChild){b+='>';var d=a.firstChild;while(d){b+=this._toSVG(d);d=d.nextSibling}b+='</'+a.nodeName+'>'}else{b+='/>'}}return b}});function SVGPath(){this._path=''}$.extend(SVGPath.prototype,{reset:function(){this._path='';return this},move:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'m':'M'),x,y)},line:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'l':'L'),x,y)},horiz:function(x,a){this._path+=(a?'h':'H')+(isArray(x)?x.join(' '):x);return this},vert:function(y,a){this._path+=(a?'v':'V')+(isArray(y)?y.join(' '):y);return this},curveC:function(a,b,c,d,x,y,e){e=(isArray(a)?b:e);return this._coords((e?'c':'C'),a,b,c,d,x,y)},smoothC:function(a,b,x,y,c){c=(isArray(a)?b:c);return this._coords((c?'s':'S'),a,b,x,y)},curveQ:function(a,b,x,y,c){c=(isArray(a)?b:c);return this._coords((c?'q':'Q'),a,b,x,y)},smoothQ:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'t':'T'),x,y)},_coords:function(a,b,c,d,e,f,g){if(isArray(b)){for(var i=0;i<b.length;i++){var h=b[i];this._path+=(i==0?a:' ')+h[0]+','+h[1]+(h.length<4?'':' '+h[2]+','+h[3]+(h.length<6?'':' '+h[4]+','+h[5]))}}else{this._path+=a+b+','+c+(d==null?'':' '+d+','+e+(f==null?'':' '+f+','+g))}return this},arc:function(a,b,c,d,e,x,y,f){f=(isArray(a)?b:f);this._path+=(f?'a':'A');if(isArray(a)){for(var i=0;i<a.length;i++){var g=a[i];this._path+=(i==0?'':' ')+g[0]+','+g[1]+' '+g[2]+' '+(g[3]?'1':'0')+','+(g[4]?'1':'0')+' '+g[5]+','+g[6]}}else{this._path+=a+','+b+' '+c+' '+(d?'1':'0')+','+(e?'1':'0')+' '+x+','+y}return this},close:function(){this._path+='z';return this},path:function(){return this._path}});SVGPath.prototype.moveTo=SVGPath.prototype.move;SVGPath.prototype.lineTo=SVGPath.prototype.line;SVGPath.prototype.horizTo=SVGPath.prototype.horiz;SVGPath.prototype.vertTo=SVGPath.prototype.vert;SVGPath.prototype.curveCTo=SVGPath.prototype.curveC;SVGPath.prototype.smoothCTo=SVGPath.prototype.smoothC;SVGPath.prototype.curveQTo=SVGPath.prototype.curveQ;SVGPath.prototype.smoothQTo=SVGPath.prototype.smoothQ;SVGPath.prototype.arcTo=SVGPath.prototype.arc;function SVGText(){this._parts=[]}$.extend(SVGText.prototype,{reset:function(){this._parts=[];return this},string:function(a){this._parts[this._parts.length]=['text',a];return this},span:function(a,b){this._parts[this._parts.length]=['tspan',a,b];return this},ref:function(a,b){this._parts[this._parts.length]=['tref',a,b];return this},path:function(a,b,c){this._parts[this._parts.length]=['textpath',b,$.extend({href:a},c||{})];return this}});$.fn.svg=function(a){var b=Array.prototype.slice.call(arguments,1);if(typeof a=='string'&&a=='get'){return $.svg['_'+a+'SVG'].apply($.svg,[this[0]].concat(b))}return this.each(function(){if(typeof a=='string'){$.svg['_'+a+'SVG'].apply($.svg,[this].concat(b))}else{$.svg._attachSVG(this,a||{})}})};function isArray(a){return(a&&a.constructor==Array)}$.svg=new SVGManager()})(jQuery);
\ No newline at end of file
diff --git a/unit/java/azkaban/scheduler/JdbcScheduleLoaderTest.java b/unit/java/azkaban/scheduler/JdbcScheduleLoaderTest.java
index 9bd48b1..5fc7235 100644
--- a/unit/java/azkaban/scheduler/JdbcScheduleLoaderTest.java
+++ b/unit/java/azkaban/scheduler/JdbcScheduleLoaderTest.java
@@ -127,12 +127,12 @@ public class JdbcScheduleLoaderTest {
 		slaOptions.setSlaEmails(emails);
 		slaOptions.setSettings(slaSets);
 		
-		Schedule s1 = new Schedule(1, "proj1", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
-		Schedule s2 = new Schedule(1, "proj1", "flow2", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "ccc", flowOptions, slaOptions);
-		Schedule s3 = new Schedule(2, "proj1", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
-		Schedule s4 = new Schedule(3, "proj2", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
-		Schedule s5 = new Schedule(3, "proj2", "flow2", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
-		Schedule s6 = new Schedule(3, "proj2", "flow3", "error", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+		Schedule s1 = new Schedule(-1, 1, "proj1", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+		Schedule s2 = new Schedule(-1, 1, "proj1", "flow2", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "ccc", flowOptions, slaOptions);
+		Schedule s3 = new Schedule(-1, 2, "proj1", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+		Schedule s4 = new Schedule(-1, 3, "proj2", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+		Schedule s5 = new Schedule(-1, 3, "proj2", "flow2", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+		Schedule s6 = new Schedule(-1, 3, "proj2", "flow3", "error", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
 		
 		loader.insertSchedule(s1);
 		loader.insertSchedule(s2);
@@ -194,14 +194,14 @@ public class JdbcScheduleLoaderTest {
 		
 		System.out.println("the flow options are " + flowOptions);
 		System.out.println("the sla options are " + slaOptions);
-		Schedule s1 = new Schedule(1, "proj1", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+		Schedule s1 = new Schedule(-1, 1, "proj1", "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
 
 		loader.insertSchedule(s1);
 		
 		emails.add("email3");
 		slaOptions.setSlaEmails(emails);
 		
-		Schedule s2 = new Schedule(1, "proj1", "flow1", "ready", 11112, "America/Los_Angeles", "2M", 22223, 33334, 44445, "cyu", flowOptions, slaOptions);
+		Schedule s2 = new Schedule(-1, 1, "proj1", "flow1", "ready", 11112, "America/Los_Angeles", "2M", 22223, 33334, 44445, "cyu", flowOptions, slaOptions);
 
 		loader.updateSchedule(s2);
 		
@@ -253,7 +253,7 @@ public class JdbcScheduleLoaderTest {
 			slaOptions.setSlaEmails(emails);
 			slaOptions.setSettings(slaSets);
 			
-			Schedule s = new Schedule(i+1, "proj"+(i+1), "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
+			Schedule s = new Schedule(-1, i+1, "proj"+(i+1), "flow1", "ready", 11111, "America/Los_Angeles", "1d", 22222, 33333, 44444, "cyu", flowOptions, slaOptions);
 			schedules.add(s);
 			try {
 				loader.insertSchedule(s);
diff --git a/unit/sql/create.schedules.sql b/unit/sql/create.schedules.sql
index 1924ecd..b759188 100644
--- a/unit/sql/create.schedules.sql
+++ b/unit/sql/create.schedules.sql
@@ -1,4 +1,5 @@
 CREATE TABLE schedules (
+	schedule_id INT NOT NULL AUTO_INCREMENT,
 	project_id INT NOT NULL,
 	project_name VARCHAR(128) NOT NULL,
 	flow_name VARCHAR(128) NOT NULL,
@@ -12,5 +13,7 @@ CREATE TABLE schedules (
 	submit_user VARCHAR(128),
 	enc_type TINYINT,
 	schedule_options LONGBLOB,
-	primary key(project_id, flow_name)
+	PRIMARY KEY (schedule_id)
 );
+
+CREATE INDEX sched_project_id ON schedules(project_id, flow_name);