diff --git a/hosting.css b/hosting.css
index fe185b8..3a84799 100644
--- a/hosting.css
+++ b/hosting.css
@@ -79,6 +79,14 @@ td.hosting-actions .hosting-button-disabled {
   text-transform: none;
 }
 
+.view-hosting-task-list .hosting-status time {
+  font-size:0.8em;
+  color: #555;
+  text-transform: uppercase;
+  padding: 0 1em 0 0;
+  white-space: nowrap;
+}
+
 /**
  * Status icons, colors.
  */
diff --git a/hosting.features.inc b/hosting.features.inc
index e6d92d8..767df39 100644
--- a/hosting.features.inc
+++ b/hosting.features.inc
@@ -529,13 +529,7 @@ function hosting_feature_rebuild_caches($features = array()) {
 
   // Record enabled features in the Hosting features registry (in
   // /var/aegir/.drush/drushrc.php)
-  $all_features = hosting_get_features();
-  foreach ($features as $feature) {
-    // Don't add a verify task during install of hostmaster site.
-    if ($all_features[$feature]['status'] == HOSTING_FEATURE_REQUIRED) {
-      return;
-    }
-  }
+
   // Allow scripts to skip automatic verify tasks by passing '--no-verify'
   if (function_exists('drush_get_option') &&
       drush_get_option('no-verify', FALSE)) {
diff --git a/platform/hosting_platform.module b/platform/hosting_platform.module
index ca94987..f749ccc 100644
--- a/platform/hosting_platform.module
+++ b/platform/hosting_platform.module
@@ -775,8 +775,6 @@ function hosting_platform_view($node, $view_mode, $langcode = NULL) {
       'changed' => $node->changed,
     );
     drupal_add_js($settings, array('type' => 'setting', 'scope' => JS_DEFAULT));
-
-    drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
   }
 
   return $node;
diff --git a/server/hosting_server.drush.inc b/server/hosting_server.drush.inc
index 49bb527..94b2264 100644
--- a/server/hosting_server.drush.inc
+++ b/server/hosting_server.drush.inc
@@ -23,7 +23,15 @@ function hosting_hosting_server_context_options(&$task) {
       }
     }
     else {
-      $task->context_options["{$type}_service_type"] = '0';
+      // If we're here, there is no enabled service of this type on this server.
+      // If such a service had previously been enabled, that fact, along with
+      // any subscribed properties would be recorded in the server context.
+      // Since we persist values in contexts, we cannot just leave this unset.
+      // Similarly, in Provision_Context::__get(), we filter out falsy values,
+      // so we cannot set this to NULL. 0 or the like. Therefore, we explicitely
+      // set it to value that we check for in
+      // Provision_Context_server::spawn_service().
+      $task->context_options["{$type}_service_type"] = 'NONE';
     }
   }
 }
diff --git a/server/hosting_server.module b/server/hosting_server.module
index cb88334..93fc732 100644
--- a/server/hosting_server.module
+++ b/server/hosting_server.module
@@ -729,8 +729,6 @@ function hosting_server_view($node, $view_mode, $langcode = NULL) {
         'changed' => $node->changed,
       );
       drupal_add_js($settings, array('type' => 'setting', 'scope' => JS_DEFAULT));
-
-      drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
     }
   }
 
diff --git a/site/hosting_site.nodeapi.inc b/site/hosting_site.nodeapi.inc
index f7dc81f..096dd3e 100644
--- a/site/hosting_site.nodeapi.inc
+++ b/site/hosting_site.nodeapi.inc
@@ -111,8 +111,6 @@ function hosting_site_view($node, $view_mode, $langcode = NULL) {
       'changed' => $node->changed,
     );
     drupal_add_js($settings, array('type' => 'setting', 'scope' => JS_DEFAULT));
-
-    drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
   }
   return $node;
 }
diff --git a/task/hosting-task-table.tpl.php b/task/hosting-task-table.tpl.php
new file mode 100644
index 0000000..63c989e
--- /dev/null
+++ b/task/hosting-task-table.tpl.php
@@ -0,0 +1,64 @@
+<table class="hosting-table">
+  <thead>
+    <tr>
+        <th scope="col">
+          <?php print t('Tasks'); ?>
+        </th>
+        <th scope="col">
+          <?php print t('Actions'); ?>
+        </th>
+    </tr>
+  </thead>
+  <tbody>
+
+    <template v-for="task in tasks">
+
+    <tr v-bind:class="task.class" v-if="!task.hidden">
+      <td class="hosting-status">
+        {{ task.title }}
+      </td>
+      <td class="hosting-actions">
+
+        <!-- View Link -->
+        <a
+          v-if="task.view_link"
+          v-bind:href="task.view_link.url"
+          v-bind:title="task.view_link.title"
+          class="hosting-button-enabled hosting-button-log hosting-button-dialog">
+            {{ task.view_link.text }}
+        </a>
+        <span
+          v-if="!task.view_link"
+          class='hosting-button-disabled'>
+          {{ task.view_link_text }}
+        </span>
+
+        <!-- Run Link -->
+        <a
+          v-if="task.task_permitted"
+          v-bind:href="task.run_link.url"
+          v-bind:title="task.run_link.title"
+          class="hosting-button-enabled hosting-button-run hosting-button-dialog">
+            {{ task.run_link.text }}
+        </a>
+
+        <span
+          v-if="!task.task_permitted"
+          class='hosting-button-disabled'>
+          {{ task.run_link.text }}
+        </span>
+
+        <!-- Cancel Link -->
+        <a
+          v-if="task.cancel_link"
+          v-bind:href="task.cancel_link.url"
+          v-bind:title="task.cancel_link.title"
+          class="hosting-button-enabled hosting-button-stop hosting-button-dialog">
+            {{ task.cancel_link.text }}
+        </a>
+      </td>
+    </tr>
+    </template>
+
+  </tbody>
+  </table>
\ No newline at end of file
diff --git a/task/hosting_task.js b/task/hosting_task.js
index ae8cfe8..1754cac 100644
--- a/task/hosting_task.js
+++ b/task/hosting_task.js
@@ -1,91 +1,169 @@
 (function($) {
 
-hostingTaskRefreshList = function() {
-  if (!Drupal.settings.hostingTaskRefresh.nid) {
-    return null;
-  }
-
-  var hostingTaskListRefreshCallback = function(data, responseText) {
-    // If the node has been modified, reload the whole page.
-    if (Drupal.settings.hostingTaskRefresh.changed < data.changed) {
-      // only reload if there is no modal frame currently open
-      if ($(document).data('hostingOpenModalFrame') != true) {
-        // If a specific URL was specified, go there.
-        if (data.navigate_url) {
-          document.location = data.navigate_url;
-        }
-        // Fall back to just doing a reload of the current page.
-        else {
-          document.location.reload();
+    Drupal.behaviors.hostingTasks = {
+        attach: function (context, settings) {
+
+            // Attach to the global hosting tasks block.
+            Drupal.settings.hostingTasks.vue = new Vue({
+                el: '#hostingTasks',
+                data: {
+                    tasks: Drupal.settings.hostingTasks.tasks,
+                },
+                watch: {
+                    tasks: function (val) {
+                        // Drupal.attachBehaviors('#hostingTasks');
+                        // Drupal.behaviors.hostingTimeAgo.attach(context, settings);
+                    },
+                }
+            });
+
+            // Attach to the available_tasks block, if there is one.
+            if ($('#hosting-task-list').length > 0) {
+                Drupal.settings.hostingTasks.vueAvailable = new Vue({
+                    el: '#hosting-task-list',
+                    data: {
+                        tasks: Drupal.settings.hostingAvailableTasks,
+                    },
+                });
+
+            }
+
+            setTimeout("Drupal.behaviors.hostingTasks.checkTasks()", settings.hostingTasks.refreshTimeout);
+        },
+        checkTasks: function () {
+            var url = Drupal.settings.hostingTasks.url;
+            $.getJSON(url, function (data) {
+
+                // Replace vue data with new data.
+                Drupal.settings.hostingTasks.vue.tasks = data.tasks;
+                if (data.availableTasks &&  Drupal.settings.hostingTasks.vueAvailable) {
+                  Drupal.settings.hostingTasks.vueAvailable.tasks = data.availableTasks;
+                }
+
+                // Stop if needed.
+                if (Drupal.settings.hostingTasks.halt != true) {
+                    setTimeout("Drupal.behaviors.hostingTasks.checkTasks()", Drupal.settings.hostingTasks.refreshTimeout);
+                }
+            });
+        },
+    };
+
+    Drupal.behaviors.hostingTimeAgo = {
+        attach: function (context, settings) {
+            $.timeago.settings.refreshMillis = 100;
+            $.timeago.settings.strings = {
+                prefixAgo: null,
+                prefixFromNow: null,
+                suffixAgo: "ago",
+                suffixFromNow: "from now",
+                inPast: 'any moment now',
+                seconds: "%d sec",
+                minute: "1 min",
+                minutes: "%d min",
+                hour: "1 hr",
+                hours: "%d hrs",
+                day: "1 day",
+                days: "%d days",
+                month: "1 month",
+                months: "%d months",
+                year: "1 year",
+                years: "%d years",
+                wordSeparator: " ",
+                numbers: []
+            }
+            $(".timeago", context).timeago();
         }
-      }
     }
-    else {
-      $("#hosting-task-list").html(data.markup);
-
-      hostingTaskBindButtons('#hosting-task-list');
-      setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
-    }
-  }
-
-  hostingTaskAddOverlay('#hosting-task-list');
-  $.get(Drupal.settings.basePath + 'hosting/tasks/' + Drupal.settings.hostingTaskRefresh.nid + '/list', null, hostingTaskListRefreshCallback , 'json' );
-}
-
-
-function hostingTaskAddOverlay(elem) {
-  $(elem).prepend('<div class="hosting-overlay"><div class="hosting-throbber"></div></div>');
-}
-
-
-hostingTaskRefreshQueueBlock = function() {
-  if (Drupal.settings.hostingTaskRefresh.queueBlock != 1) {
-    return null;
-  }
-
-  var hostingTaskQueueRefreshCallback = function(data, responseText) {
-    $("#block-views-hosting-task-list-block .content").html(data.markup);
-
-    hostingTaskBindButtons('#block-views-hosting-task-list-block');
-    setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
-  }
-
-  hostingTaskAddOverlay('#block-views-hosting-task-list-block .view-content');
-  $.get(Drupal.settings.basePath + 'hosting/tasks/queue', null, hostingTaskQueueRefreshCallback , 'json');
-}
-
-$(document).ready(function() {
-  $(document).data('hostingOpenModalFrame', false);
-  setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
-  setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
-  hostingTaskBindButtons($(this));
-  $('#hosting-task-confirm-form-actions a').click(function() {
-    if (parent.Drupal.modalFrame.isOpen) {
-      setTimeout(function() { parent.Drupal.modalFrame.close({}, {}); }, 1);
-      return false;
-    }
-  });
-
-});
-
-hostingTaskBindButtons = function(elem) {
-  $('.hosting-button-dialog', elem).click(function() {
-      $(document).data('hostingOpenModalFrame', true)
-     var options = {
-        url : Drupal.settings.basePath + 'hosting/js' + $(this).attr('href'),
-        draggable : false,
-        width : 600,
-        height : 150,
-        onSubmit : function() {
-          $(document).data('hostingOpenModalFrame', false)
-          hostingTaskRefreshQueueBlock();
-          hostingTaskRefreshList();
-        }
-      }
-      Drupal.modalFrame.open(options);
-      return false;
-   });
-}
-
-
-})(jQuery);
+}(jQuery));
+
+// (function($) {
+//
+// hostingTaskRefreshList = function() {
+//   if (!Drupal.settings.hostingTaskRefresh.nid) {
+//     return null;
+//   }
+//
+//   var hostingTaskListRefreshCallback = function(data, responseText) {
+//     // If the node has been modified, reload the whole page.
+//     if (Drupal.settings.hostingTaskRefresh.changed < data.changed) {
+//       // only reload if there is no modal frame currently open
+//       if ($(document).data('hostingOpenModalFrame') != true) {
+//         // If a specific URL was specified, go there.
+//         if (data.navigate_url) {
+//           document.location = data.navigate_url;
+//         }
+//         // Fall back to just doing a reload of the current page.
+//         else {
+//           document.location.reload();
+//         }
+//       }
+//     }
+//     else {
+//       $("#hosting-task-list").html(data.markup);
+//
+//       hostingTaskBindButtons('#hosting-task-list');
+//       setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
+//     }
+//   }
+//
+//   hostingTaskAddOverlay('#hosting-task-list');
+//   $.get(Drupal.settings.basePath + 'hosting/tasks/' + Drupal.settings.hostingTaskRefresh.nid + '/list', null, hostingTaskListRefreshCallback , 'json' );
+// }
+//
+//
+// function hostingTaskAddOverlay(elem) {
+//   $(elem).prepend('<div class="hosting-overlay"><div class="hosting-throbber"></div></div>');
+// }
+//
+//
+// hostingTaskRefreshQueueBlock = function() {
+//   if (Drupal.settings.hostingTaskRefresh.queueBlock != 1) {
+//     return null;
+//   }
+//
+//   var hostingTaskQueueRefreshCallback = function(data, responseText) {
+//     // $("#block-views-hosting-task-list-block .content").html(data.markup);
+//     //
+//     // hostingTaskBindButtons('#block-views-hosting-task-list-block');
+//     setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
+//   }
+//
+//   // hostingTaskAddOverlay('#block-views-hosting-task-list-block .view-content');
+//   $.get(Drupal.settings.basePath + 'hosting/tasks/queue', null, hostingTaskQueueRefreshCallback , 'json');
+// }
+//
+// $(document).ready(function() {
+//   $(document).data('hostingOpenModalFrame', false);
+//   setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
+//   setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout);
+//   hostingTaskBindButtons($(this));
+//   $('#hosting-task-confirm-form-actions a').click(function() {
+//     if (parent.Drupal.modalFrame.isOpen) {
+//       setTimeout(function() { parent.Drupal.modalFrame.close({}, {}); }, 1);
+//       return false;
+//     }
+//   });
+//
+// });
+//
+// hostingTaskBindButtons = function(elem) {
+//   $('.hosting-button-dialog', elem).click(function() {
+//       $(document).data('hostingOpenModalFrame', true)
+//      var options = {
+//         url : Drupal.settings.basePath + 'hosting/js' + $(this).attr('href'),
+//         draggable : false,
+//         width : 600,
+//         height : 150,
+//         onSubmit : function() {
+//           $(document).data('hostingOpenModalFrame', false)
+//           hostingTaskRefreshQueueBlock();
+//           hostingTaskRefreshList();
+//         }
+//       }
+//       Drupal.modalFrame.open(options);
+//       return false;
+//    });
+// }
+//
+//
+// })(jQuery);
diff --git a/task/hosting_task.module b/task/hosting_task.module
index 598ae49..42e88f7 100644
--- a/task/hosting_task.module
+++ b/task/hosting_task.module
@@ -10,10 +10,10 @@
  * Adds refreshTimeout javascript variable.
  */
 function hosting_task_init() {
-  $settings['hostingTaskRefresh'] = array(
-      'refreshTimeout' => variable_get('hosting_task_refresh_timeout', 30000),
-  );
-  drupal_add_js($settings, 'setting');
+//  $settings['hostingTaskRefresh'] = array(
+//      'refreshTimeout' => variable_get('hosting_task_refresh_timeout', 30000),
+//  );
+//  drupal_add_js($settings, 'setting');
 }
 
 
@@ -48,13 +48,12 @@ function hosting_task_menu() {
     }
   }
 
-  $items['hosting/tasks/%node/list'] = array(
+  $items['hosting/json/tasks'] = array(
     'title' => 'Task list',
     'description' => 'AJAX callback for refreshing task list',
     'page callback' => 'hosting_task_ajax_list',
-    'page arguments' => array(2),
-    'access callback' => 'node_access',
-    'access arguments' => array('view', 2),
+    'page arguments' => array(3),
+    'access arguments' => array('access task logs'),
     'type' => MENU_CALLBACK,
   );
 
@@ -68,14 +67,6 @@ function hosting_task_menu() {
     'type' => MENU_CALLBACK,
   );
 
-  $items['hosting/tasks/queue'] = array(
-    'title' => 'Task list',
-    'description' => 'AJAX callback for refreshing task queue',
-    'page callback' => 'hosting_task_ajax_queue',
-    'access arguments' => array('access task logs'),
-    'type' => MENU_CALLBACK,
-  );
-
   // Custom path to task node views for overlay.
   // See hosting_task_overlay_paths().
   $items['hosting/task/%node'] = array(
@@ -192,23 +183,20 @@ function hosting_task_ajax_command_hosting_table_check($selector, $url, $setting
 /**
  * Page callback to provide JSON output for a task.
  */
-function hosting_task_ajax_list($node) {
-  $return['markup'] = hosting_task_table($node);
-  $return['changed'] = $node->changed;
-  $return['navigate_url'] = url('node/' . $node->nid);
-  drupal_json_output($return);
-  exit();
-}
+function hosting_task_ajax_list($nid = NULL) {
 
-/**
- * AJAX callback for refreshing task list.
- */
-function hosting_task_ajax_queue() {
-  $view = views_get_view('hosting_task_list');
-  $view->set_display('block');
-  $view->pre_execute();
-  $return['markup'] = $view->render('block');
+  // Load available tasks if a node is specified and accessible.
+  $node = node_load($nid);
+  if ($node && node_access('view', $node)) {
+    $return['availableTasks'] =  array_values(hosting_task_fetch_tasks($node->nid));
+    $return['changed'] = $node->changed;
+    $return['navigate_url'] = url('node/' . $node->nid);
+  }
+
+  // Load global tasks for block.
+  $return['tasks'] = views_get_view_result('hosting_task_list', 'block');
 
+  // Output JSON
   drupal_json_output($return);
   exit();
 }
@@ -584,7 +572,6 @@ function hosting_add_task($nid, $type, $args = NULL, $status = HOSTING_TASK_QUEU
  * Implements hook_form().
  */
 function hosting_task_confirm_form($form, $form_state, $node, $task) {
-  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
   $tasks = hosting_available_tasks($node->type);
 
   if (!isset($tasks[$task]['dialog']) || !$tasks[$task]['dialog']) {
@@ -1459,56 +1446,12 @@ function hosting_task_list($filter_by = NULL, $filter_value = NULL) {
  * simple interface.
  */
 function hosting_task_table($node) {
-  $output = '';
-
-  $headers[] = t('Task');
-  $headers[] = array(
-    'data' => t('Actions'),
-    'class' => array('hosting-actions'),
-  );
-
-  $tasklist = hosting_task_fetch_tasks($node->nid);
-  $rows = array();
-
-  foreach ($tasklist as $task => $info) {
-    $row = array();
-
-    if (!isset($info['nid']) && !$info['task_permitted']) {
-      // Just don't show those tasks, since we'll not be able to run them.
-      continue;
-    }
-
-    if (empty($info['title'])) {
-      // Skip tasks from types that have since been removed.
-      continue;
-    }
-
-    $row['type'] = array(
-      'data' => $info['title'],
-      'class' => array('hosting-status'),
-    );
-    $actions = array();
-
-    if (isset($info['task_status']) && ($info['task_status'] == 0)) {
-      $actions['cancel'] = _hosting_task_button(t('Cancel'), sprintf("hosting/tasks/%d/cancel", $info['nid']), t("Cancel the task and remove it from the queue"), 'hosting-button-stop', !$info['task_permitted']);
-    }
-    else {
-      $actions['run'] = _hosting_task_button(t('Run'), sprintf("hosting_confirm/%d/%s_%s", $node->nid, $node->type, $task), $info['description'], 'hosting-button-run', $info['task_permitted'], $info['dialog']);
-    }
-
-    $actions['log'] = _hosting_task_button(t('View'), isset($info['nid']) ? 'hosting/task/' . $info['nid'] : '<front>', t("Display the task log"), 'hosting-button-log', isset($info['nid']) &&  user_access('access task logs'), TRUE, FALSE);
-    $row['actions'] = array(
-      'data' => implode('', $actions),
-      'class' => array('hosting-actions'),
-    );
-
-    $rows[] = array(
-      'data' => $row,
-      'class' => array($info['class']),
-    );
-  }
-  $output .= theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('class' => array('hosting-table'))));
-  return $output;
+  $tasks = hosting_task_fetch_tasks($node->nid);
+  $settings['hostingTasks']['availableTasks'] = $tasks;
+  drupal_add_js($settings, 'setting');
+  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
+  drupal_add_js('https://npmcdn.com/vue/dist/vue.js', 'external');
+  return theme('hosting_task_table');
 }
 
 /**
@@ -1630,6 +1573,8 @@ function hosting_task_fetch_tasks($rid) {
   $tasks = hosting_available_tasks($node->type);
   ksort($tasks);
 
+  global $user;
+
   foreach ($tasks as $type => $hook_task) {
 
     if (!isset($return[$type])) {
@@ -1641,6 +1586,8 @@ function hosting_task_fetch_tasks($rid) {
     $task = array();
     $task = array_merge($return[$type], $hook_task);
 
+    $task['task_type'] = $type;
+
     $allowed = (isset($task['exists']) && !in_array($task['task_status'], array(HOSTING_TASK_QUEUED, HOSTING_TASK_PROCESSING))) || !isset($task['exists']);
     if ($allowed && empty($task['hidden']) && $access_callback($node, $type)) {
       $task['task_permitted'] = TRUE;
@@ -1657,12 +1604,61 @@ function hosting_task_fetch_tasks($rid) {
     }
     $task['class'] = hosting_task_status_class($task['task_status']);
 
+    // Generate Links
+    $task['view_link'] = FALSE;
+    $task['view_link_text'] = t('View');
+    $task['run_link'] = FALSE;
+    $task['cancel_link'] = FALSE;
+
+    $nid = $task['nid'];
+    $ref_type = $node->type;
+    $task_type = $task['task_type'];
+
+    // View Logs link
+    if (!empty($nid) && drupal_valid_path("hosting/task/{$nid}")) {
+      $task['view_link'] = array(
+        'url' => url("hosting/task/{$nid}"),
+        'title' => t('Display the task log.'),
+        'text' => t('View'),
+      );
+    }
+
+    // Cancel Task link
+    if (isset($task['task_status']) && ($task['task_status'] === HOSTING_TASK_QUEUED)) {
+//      $actions['cancel'] = _hosting_task_button(t('Cancel'), sprintf("hosting/tasks/%d/cancel", $info['nid']), t("Cancel the task and remove it from the queue"), 'hosting-button-stop', !$info['task_permitted']);
+
+      $task['cancel_link'] = array(
+        'url' => url("hosting/tasks/{$nid}/cancel"),
+        'title' => t('Cancel the task and remove it from the queue.'),
+        'text' => t('Cancel'),
+      );
+  }
+  else {
+//    $actions['run'] = _hosting_task_button(t('Run'), sprintf("hosting_confirm/%d/%s_%s", $node->nid, $node->type, $task), $info['description'], 'hosting-button-run', $info['task_permitted'], $info['dialog']);
+    $task['run_link'] = array(
+      'url' => url("hosting_confirm/{$rid}/{$ref_type}_{$task_type}", array(
+        'query'=> array(
+          'token' => drupal_get_token($user->uid),
+        )
+      )),
+      'title' => $task['description'],
+      'text' => t('Run'),
+    );
+  }
+
+
+    if (empty($nid) && $task['task_permitted'] == FALSE) {
+      $task['hidden'] = TRUE;
+    }
+
     $return[$type] = $task;
   }
 
   return $return;
 }
 
+
+
 /**
  * Traslate a task status code into a css class.
  */
@@ -1698,6 +1694,7 @@ function hosting_task_views_api() {
   return array(
     'api' => 3,
     'path' => drupal_get_path('module', 'hosting_task') . '/includes/views',
+    'template path' => drupal_get_path('module', 'hosting_task'),
   );
 }
 
@@ -1724,17 +1721,61 @@ function hosting_task_preprocess_views_view_table(&$vars) {
 
   switch ($id) {
     case 'hosting_task_list-block':
-      drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
-
       $settings['hostingTaskRefresh'] = array(
         'queueBlock' => 1,
       );
-      drupal_add_js($settings, 'setting');
+
       break;
   }
 }
 
 /**
+ * Implements hook_preprocess_HOOK().
+ */
+function hosting_task_preprocess_page(&$variables) {
+  $settings['hostingTasks'] = array(
+    'url' => url("hosting/json/tasks"),
+    'refreshTimeout' => 2000,
+    'tasks' => views_get_view_result('hosting_task_list', 'block'),
+  );
+
+  // If on a hosting node page...
+  if (isset($variables['node']) && !empty($variables['node']->nid)) {
+    $availableTasks = hosting_task_fetch_tasks($variables['node']->nid);
+
+    // Get rid of array keys so it becomes an array.
+    $settings['hostingAvailableTasks'] = array_values($availableTasks);
+    $settings['hostingTasks']['url'] = url("hosting/json/tasks/" . $variables['node']->nid);
+  }
+
+  drupal_add_js($settings, 'setting');
+  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');
+  drupal_add_js('https://npmcdn.com/vue/dist/vue.js', 'external');
+  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/js/jquery.timeago.js');
+}
+
+/**
+ * Implements hook_views_post_execute()
+ *
+ * Used to modify the results of the hosting_task_list view to add links, text,
+ * and readable timestamps.
+ */
+function hosting_task_views_post_execute(&$view) {
+
+  // Parse Results on hosting_task_list view.
+  if ($view->name == 'hosting_task_list') {
+    foreach ($view->result as $i => &$result) {
+      $result->status_class = hosting_task_status_class($result->hosting_task_task_status);
+      $result->ref_url = url("node/$result->node_hosting_task_nid");
+      $result->task_url = url("hosting/task/$result->nid");
+      $result->task_link_text = t('View');
+      $result->timestamp = date('c', $result->node_created);
+      $result->timestamp_ago = t('!time ago', array('!time' => format_interval(time() - $result->node_revision_timestamp, 1)));
+    }
+  }
+}
+
+/**
  * Set a task's status according to its log.
  *
  * @param object|int $task
@@ -1892,3 +1933,18 @@ function hosting_task_entity_property_info_alter(&$info) {
   );
   */
 }
+
+/**
+ * Implements hook_theme().
+ */
+function hosting_task_theme($existing, $type, $theme, $path) {
+  $theme = array();
+  $theme['hosting_task_table'] = array(
+      'template' => 'hosting-task-table',
+      'variables' => array(
+        array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => TRUE, 'empty' => ''),
+      'path' => drupal_get_path('module', 'hosting_task'),
+    )
+  );
+  return $theme;
+}
\ No newline at end of file
diff --git a/task/includes/views/hosting_task.views_default.inc b/task/includes/views/hosting_task.views_default.inc
index 58a7d7c..0ce466f 100644
--- a/task/includes/views/hosting_task.views_default.inc
+++ b/task/includes/views/hosting_task.views_default.inc
@@ -134,6 +134,21 @@ function hosting_task_views_default_views() {
   $handler->display->display_options['fields']['created']['exclude'] = TRUE;
   $handler->display->display_options['fields']['created']['element_label_colon'] = FALSE;
   $handler->display->display_options['fields']['created']['date_format'] = 'long';
+  /* Field: Content: Type */
+  $handler->display->display_options['fields']['type']['id'] = 'type';
+  $handler->display->display_options['fields']['type']['table'] = 'node';
+  $handler->display->display_options['fields']['type']['field'] = 'type';
+  $handler->display->display_options['fields']['type']['relationship'] = 'rid';
+  $handler->display->display_options['fields']['type']['exclude'] = TRUE;
+  /* Field: Output */
+  $handler->display->display_options['fields']['task_type']['id'] = 'task_type';
+  $handler->display->display_options['fields']['task_type']['table'] = 'hosting_task';
+  $handler->display->display_options['fields']['task_type']['field'] = 'task_type';
+  $handler->display->display_options['fields']['task_type']['ui_name'] = 'Output';
+  $handler->display->display_options['fields']['task_type']['label'] = 'Task';
+  $handler->display->display_options['fields']['task_type']['alter']['alter_text'] = TRUE;
+  $handler->display->display_options['fields']['task_type']['alter']['text'] = '[task_type] [type]: [title]';
+  $handler->display->display_options['fields']['task_type']['element_class'] = 'hosting-status';
   /* Sort criterion: Content: Updated date */
   $handler->display->display_options['sorts']['changed']['id'] = 'changed';
   $handler->display->display_options['sorts']['changed']['table'] = 'node';
diff --git a/task/js/jquery.timeago.js b/task/js/jquery.timeago.js
new file mode 100644
index 0000000..c85ebcc
--- /dev/null
+++ b/task/js/jquery.timeago.js
@@ -0,0 +1,229 @@
+/**
+ * Timeago is a jQuery plugin that makes it easy to support automatically
+ * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
+ *
+ * @name timeago
+ * @version 1.5.3
+ * @requires jQuery v1.2.3+
+ * @author Ryan McGeary
+ * @license MIT License - http://www.opensource.org/licenses/mit-license.php
+ *
+ * For usage and examples, visit:
+ * http://timeago.yarp.com/
+ *
+ * Copyright (c) 2008-2015, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
+ */
+
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof module === 'object' && typeof module.exports === 'object') {
+    factory(require('jquery'));
+  } else {
+    // Browser globals
+    factory(jQuery);
+  }
+}(function ($) {
+  $.timeago = function(timestamp) {
+    if (timestamp instanceof Date) {
+      return inWords(timestamp);
+    } else if (typeof timestamp === "string") {
+      return inWords($.timeago.parse(timestamp));
+    } else if (typeof timestamp === "number") {
+      return inWords(new Date(timestamp));
+    } else {
+      return inWords($.timeago.datetime(timestamp));
+    }
+  };
+  var $t = $.timeago;
+
+  $.extend($.timeago, {
+    settings: {
+      refreshMillis: 60000,
+      allowPast: true,
+      allowFuture: false,
+      localeTitle: false,
+      cutoff: 0,
+      autoDispose: true,
+      strings: {
+        prefixAgo: null,
+        prefixFromNow: null,
+        suffixAgo: "ago",
+        suffixFromNow: "from now",
+        inPast: 'any moment now',
+        seconds: "less than a minute",
+        minute: "about a minute",
+        minutes: "%d minutes",
+        hour: "about an hour",
+        hours: "about %d hours",
+        day: "a day",
+        days: "%d days",
+        month: "about a month",
+        months: "%d months",
+        year: "about a year",
+        years: "%d years",
+        wordSeparator: " ",
+        numbers: []
+      }
+    },
+
+    inWords: function(distanceMillis) {
+      if (!this.settings.allowPast && ! this.settings.allowFuture) {
+          throw 'timeago allowPast and allowFuture settings can not both be set to false.';
+      }
+
+      var $l = this.settings.strings;
+      var prefix = $l.prefixAgo;
+      var suffix = $l.suffixAgo;
+      if (this.settings.allowFuture) {
+        if (distanceMillis < 0) {
+          prefix = $l.prefixFromNow;
+          suffix = $l.suffixFromNow;
+        }
+      }
+
+      if (!this.settings.allowPast && distanceMillis >= 0) {
+        return this.settings.strings.inPast;
+      }
+
+      var seconds = Math.abs(distanceMillis) / 1000;
+      var minutes = seconds / 60;
+      var hours = minutes / 60;
+      var days = hours / 24;
+      var years = days / 365;
+
+      function substitute(stringOrFunction, number) {
+        var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
+        var value = ($l.numbers && $l.numbers[number]) || number;
+        return string.replace(/%d/i, value);
+      }
+
+      var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
+        seconds < 90 && substitute($l.minute, 1) ||
+        minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
+        minutes < 90 && substitute($l.hour, 1) ||
+        hours < 24 && substitute($l.hours, Math.round(hours)) ||
+        hours < 42 && substitute($l.day, 1) ||
+        days < 30 && substitute($l.days, Math.round(days)) ||
+        days < 45 && substitute($l.month, 1) ||
+        days < 365 && substitute($l.months, Math.round(days / 30)) ||
+        years < 1.5 && substitute($l.year, 1) ||
+        substitute($l.years, Math.round(years));
+
+      var separator = $l.wordSeparator || "";
+      if ($l.wordSeparator === undefined) { separator = " "; }
+      return $.trim([prefix, words, suffix].join(separator));
+    },
+
+    parse: function(iso8601) {
+      var s = $.trim(iso8601);
+      s = s.replace(/\.\d+/,""); // remove milliseconds
+      s = s.replace(/-/,"/").replace(/-/,"/");
+      s = s.replace(/T/," ").replace(/Z/," UTC");
+      s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
+      s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
+      return new Date(s);
+    },
+    datetime: function(elem) {
+      var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
+      return $t.parse(iso8601);
+    },
+    isTime: function(elem) {
+      // jQuery's `is()` doesn't play well with HTML5 in IE
+      return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
+    }
+  });
+
+  // functions that can be called via $(el).timeago('action')
+  // init is default when no action is given
+  // functions are called with context of a single element
+  var functions = {
+    init: function() {
+      var refresh_el = $.proxy(refresh, this);
+      refresh_el();
+      var $s = $t.settings;
+      if ($s.refreshMillis > 0) {
+        this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
+      }
+    },
+    update: function(timestamp) {
+      var date = (timestamp instanceof Date) ? timestamp : $t.parse(timestamp);
+      $(this).data('timeago', { datetime: date });
+      if ($t.settings.localeTitle) $(this).attr("title", date.toLocaleString());
+      refresh.apply(this);
+    },
+    updateFromDOM: function() {
+      $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
+      refresh.apply(this);
+    },
+    dispose: function () {
+      if (this._timeagoInterval) {
+        window.clearInterval(this._timeagoInterval);
+        this._timeagoInterval = null;
+      }
+    }
+  };
+
+  $.fn.timeago = function(action, options) {
+    var fn = action ? functions[action] : functions.init;
+    if (!fn) {
+      throw new Error("Unknown function name '"+ action +"' for timeago");
+    }
+    // each over objects here and call the requested function
+    this.each(function() {
+      fn.call(this, options);
+    });
+    return this;
+  };
+
+  function refresh() {
+    var $s = $t.settings;
+
+    //check if it's still visible
+    if ($s.autoDispose && !$.contains(document.documentElement,this)) {
+      //stop if it has been removed
+      $(this).timeago("dispose");
+      return this;
+    }
+
+    var data = prepareData(this);
+
+    if (!isNaN(data.datetime)) {
+      if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
+        $(this).text(inWords(data.datetime));
+      } else {
+        if ($(this).attr('title').length > 0) {
+            $(this).text($(this).attr('title'));
+        }
+      }
+    }
+    return this;
+  }
+
+  function prepareData(element) {
+    element = $(element);
+    if (!element.data("timeago")) {
+      element.data("timeago", { datetime: $t.datetime(element) });
+      var text = $.trim(element.text());
+      if ($t.settings.localeTitle) {
+        element.attr("title", element.data('timeago').datetime.toLocaleString());
+      } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
+        element.attr("title", text);
+      }
+    }
+    return element.data("timeago");
+  }
+
+  function inWords(date) {
+    return $t.inWords(distance(date));
+  }
+
+  function distance(date) {
+    return (new Date().getTime() - date.getTime());
+  }
+
+  // fix for IE6 suckage
+  document.createElement("abbr");
+  document.createElement("time");
+}));
diff --git a/task/views-view-table--hosting-task-list--block.tpl.php b/task/views-view-table--hosting-task-list--block.tpl.php
new file mode 100644
index 0000000..aaacc56
--- /dev/null
+++ b/task/views-view-table--hosting-task-list--block.tpl.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Template to display a view as a table.
+ *
+ * - $title : The title of this group of rows.  May be empty.
+ * - $header: An array of header labels keyed by field id.
+ * - $caption: The caption for this table. May be empty.
+ * - $header_classes: An array of header classes keyed by field id.
+ * - $fields: An array of CSS IDs to use for each field id.
+ * - $classes: A class or classes to apply to the table, based on settings.
+ * - $row_classes: An array of classes to apply to each row, indexed by row
+ *   number. This matches the index in $rows.
+ * - $rows: An array of row items. Each row is an array of content.
+ *   $rows are keyed by row number, fields within rows are keyed by field ID.
+ * - $field_classes: An array of classes to apply to each field, indexed by
+ *   field id, then row number. This matches the index in $rows.
+ * @ingroup views_templates
+ */
+?>
+
+<table <?php if ($classes) { print 'class="'. $classes . '" '; } ?><?php print $attributes; ?> id="hostingTasks">
+  <?php if (!empty($title) || !empty($caption)) : ?>
+    <caption><?php print $caption . $title; ?></caption>
+  <?php endif; ?>
+  <?php if (!empty($header)) : ?>
+    <thead>
+    <tr>
+      <?php foreach ($header as $field => $label): ?>
+        <th <?php if ($header_classes[$field]) { print 'class="'. $header_classes[$field] . '" '; } ?> scope="col">
+          <?php print $label; ?>
+        </th>
+      <?php endforeach; ?>
+    </tr>
+    </thead>
+  <?php endif; ?>
+  <tbody>
+    <template v-for="task in tasks">
+    <tr v-bind:class="task.status_class">
+      <td class="hosting-status">
+        <span class="views-field-task-type">{{ task.hosting_task_task_type }} {{ task.node_hosting_task_type }}:</span> <a class="reference" v-bind:href="task.ref_url">{{ task.node_hosting_task_title }}</a>
+        <time class="timeago" v-bind:datetime="task.timestamp">{{ task.timestamp }}</time>
+      </td>
+      <td class="views-field views-field-nid hosting-actions">
+        <a v-bind:href="task.task_url" class="hosting-button-enabled hosting-button-log hosting-button-dialog">{{ task.task_link_text }}</a>
+      </td>
+    </tr>
+    </template>
+  </tbody>
+</table>
