diff --git a/frontend/src/metabase/admin/routes.jsx b/frontend/src/metabase/admin/routes.jsx
index 503f0c1df37a88c8ac980511264eb96c94ddfbd8..e0b7a79d872e219174b109c94fb87d3a2f68ae19 100644
--- a/frontend/src/metabase/admin/routes.jsx
+++ b/frontend/src/metabase/admin/routes.jsx
@@ -4,6 +4,7 @@ import { IndexRoute, IndexRedirect } from "react-router";
 import { t } from "c-3po";
 
 import { withBackground } from "metabase/hoc/Background";
+import { ModalRoute } from "metabase/hoc/ModalRoute";
 
 // Settings
 import SettingsEditorApp from "metabase/admin/settings/containers/SettingsEditorApp.jsx";
@@ -21,6 +22,9 @@ import AdminPeopleApp from "metabase/admin/people/containers/AdminPeopleApp.jsx"
 import FieldApp from "metabase/admin/datamodel/containers/FieldApp.jsx";
 import TableSettingsApp from "metabase/admin/datamodel/containers/TableSettingsApp.jsx";
 
+import TasksApp from "metabase/admin/tasks/containers/TasksApp";
+import TaskModal from "metabase/admin/tasks/containers/TaskModal";
+
 // People
 import PeopleListingApp from "metabase/admin/people/containers/PeopleListingApp.jsx";
 import GroupsListingApp from "metabase/admin/people/containers/GroupsListingApp.jsx";
@@ -75,6 +79,14 @@ const getRoutes = (store, IsAdmin) => (
       </Route>
     </Route>
 
+    {/* Troubleshooting */}
+    <Route path="troubleshooting" title={t`Troubleshooting`}>
+      <IndexRedirect to="tasks" />
+      <Route path="tasks" component={TasksApp}>
+        <ModalRoute path=":taskId" modal={TaskModal} />
+      </Route>
+    </Route>
+
     {/* SETTINGS */}
     <Route path="settings" title={t`Settings`}>
       <IndexRedirect to="/admin/settings/setup" />
diff --git a/frontend/src/metabase/admin/tasks/containers/TaskModal.jsx b/frontend/src/metabase/admin/tasks/containers/TaskModal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..cdc206e7c37ec65589dbeb7561b8f78c2c9c8d10
--- /dev/null
+++ b/frontend/src/metabase/admin/tasks/containers/TaskModal.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { t } from "c-3po";
+import { connect } from "react-redux";
+import { goBack } from "react-router-redux";
+
+import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader";
+
+import Code from "metabase/components/Code";
+import ModalContent from "metabase/components/ModalContent";
+
+@entityObjectLoader({
+  entityType: "tasks",
+  entityId: (state, props) => props.params.taskId,
+})
+@connect(null, { goBack })
+class TaskModal extends React.Component {
+  render() {
+    const { object } = this.props;
+    return (
+      <ModalContent title={t`Task details`} onClose={() => this.props.goBack()}>
+        <Code>{JSON.stringify(object.task_details)}</Code>
+      </ModalContent>
+    );
+  }
+}
+
+export default TaskModal;
diff --git a/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx b/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c554ca324a7b0298c3377d364fcf380fea54710f
--- /dev/null
+++ b/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx
@@ -0,0 +1,97 @@
+import React from "react";
+import { t } from "c-3po";
+import { Box, Flex } from "grid-styled";
+
+import { entityListLoader } from "metabase/entities/containers/EntityListLoader";
+
+import AdminHeader from "metabase/components/AdminHeader";
+import Icon, { IconWrapper } from "metabase/components/Icon";
+import Link from "metabase/components/Link";
+import Tooltip from "metabase/components/Tooltip";
+
+@entityListLoader({
+  entityType: "tasks",
+  pageSize: 50,
+})
+class TasksApp extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      offset: this.props.entityQuery.offset,
+    };
+  }
+  render() {
+    const {
+      tasks,
+      page,
+      pageSize,
+      onNextPage,
+      onPreviousPage,
+      children,
+    } = this.props;
+    return (
+      <Box p={3}>
+        <Flex align="center">
+          <Flex align="center">
+            <AdminHeader title={t`Troubleshooting logs`} />
+            <Tooltip
+              tooltip={t`Trying to get to the bottom of something? This section shows logs of Metabase's background tasks, which can help shed light on what's going on.`}
+            >
+              <Icon
+                name="info"
+                ml={1}
+                style={{ marginTop: 5 }}
+                className="text-brand-hover cursor-pointer text-medium"
+              />
+            </Tooltip>
+          </Flex>
+          <Flex align="center" ml="auto">
+            <span className="text-bold mr1">
+              {page * pageSize + 1} - {page * pageSize + tasks.length}
+            </span>
+            <IconWrapper onClick={onPreviousPage} disabled={!onPreviousPage}>
+              <Icon name="chevronleft" />
+            </IconWrapper>
+            <IconWrapper small onClick={onNextPage} disabled={!onNextPage}>
+              <Icon name="chevronright" />
+            </IconWrapper>
+          </Flex>
+        </Flex>
+
+        <table className="ContentTable mt2">
+          <thead>
+            <th>{t`Task`}</th>
+            <th>{t`DB ID`}</th>
+            <th>{t`Started at`}</th>
+            <th>{t`Ended at`}</th>
+            <th>{t`Duration (ms)`}</th>
+            <th>{t`Details`}</th>
+          </thead>
+          <tbody>
+            {tasks.map(task => (
+              <tr key={task.id}>
+                <td className="text-bold">{task.task}</td>
+                <td>{task.db_id}</td>
+                <td>{task.started_at}</td>
+                <td>{task.ended_at}</td>
+                <td>{task.duration}</td>
+                <td>
+                  <Link
+                    className="link text-bold"
+                    to={`/admin/troubleshooting/tasks/${task.id}`}
+                  >{t`View`}</Link>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+        {
+          // render 'children' so that the invididual task modals show up
+          children
+        }
+      </Box>
+    );
+  }
+}
+
+export default TasksApp;
diff --git a/frontend/src/metabase/containers/CollectionListLoader.jsx b/frontend/src/metabase/containers/CollectionListLoader.jsx
index c39083defec6df697d5d048a039cc0d56101694d..5fded6930df18bc19c8aa3932de59a74502a1d96 100644
--- a/frontend/src/metabase/containers/CollectionListLoader.jsx
+++ b/frontend/src/metabase/containers/CollectionListLoader.jsx
@@ -11,7 +11,6 @@ const CollectionListLoader = ({ children, writable, ...props }: Props) => (
   <EntityListLoader
     entityType="collections"
     {...props}
-    // $FlowFixMe: flow doesn't know about dyanmically generated "collections" prop
     children={({ list, collections, ...props }) =>
       children({
         list: writable ? list && list.filter(c => c.can_write) : list,
diff --git a/frontend/src/metabase/css/core/text.css b/frontend/src/metabase/css/core/text.css
index d4fc21679781f7dd3c8eaf80d5f0acc9ec9e6934..4087dd67b4c8f66c42bfc17f523d86d1d0c9eee9 100644
--- a/frontend/src/metabase/css/core/text.css
+++ b/frontend/src/metabase/css/core/text.css
@@ -175,7 +175,7 @@
   border-radius: 2px;
   padding: 0.2em 0.4em;
   line-height: 1.4em;
-  white-space: pre;
+  white-space: pre-wrap;
 }
 
 .text-monospace,
diff --git a/frontend/src/metabase/entities/containers/EntityListLoader.jsx b/frontend/src/metabase/entities/containers/EntityListLoader.jsx
index 31987dd1e3f31365ad4d1cd3fd74f1d069062f4b..dc6e322daecd4ea705aab508d8b51f09c1128304 100644
--- a/frontend/src/metabase/entities/containers/EntityListLoader.jsx
+++ b/frontend/src/metabase/entities/containers/EntityListLoader.jsx
@@ -7,6 +7,7 @@ import { createSelector } from "reselect";
 import { createMemoizedSelector } from "metabase/lib/redux";
 
 import entityType from "./EntityType";
+import paginationState from "metabase/hoc/PaginationState";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
 export type Props = {
@@ -41,9 +42,13 @@ const getMemoizedEntityQuery = createMemoizedSelector(
 );
 
 @entityType()
+@paginationState()
 @connect((state, props) => {
-  const { entityDef } = props;
-  const entityQuery = getMemoizedEntityQuery(state, props);
+  let { entityDef, entityQuery, page, pageSize } = props;
+  if (typeof pageSize === "number" && typeof page === "number") {
+    entityQuery = { limit: pageSize, offset: pageSize * page, ...entityQuery };
+  }
+  entityQuery = getMemoizedEntityQuery(state, { entityQuery });
   return {
     entityQuery,
     list: entityDef.selectors.getList(state, { entityQuery }),
@@ -74,22 +79,33 @@ export default class EntityListLoader extends React.Component {
     );
   }
 
+  async fetchList(
+    // $FlowFixMe: fetchList provided by @connect
+    { fetchList, entityQuery, pageSize, onChangeHasMorePages },
+    options?: any,
+  ) {
+    const result = await fetchList(entityQuery, options);
+    if (typeof pageSize === "number" && onChangeHasMorePages) {
+      onChangeHasMorePages(
+        !result.payload.result || result.payload.result.length === pageSize,
+      );
+    }
+    return result;
+  }
+
   componentWillMount() {
-    // $FlowFixMe: provided by @connect
-    this.props.fetchList(this.props.entityQuery, { reload: this.props.reload });
+    this.fetchList(this.props, { reload: this.props.reload });
   }
 
   componentWillReceiveProps(nextProps: Props) {
     if (!_.isEqual(nextProps.entityQuery, this.props.entityQuery)) {
       // entityQuery changed, reload
-      // $FlowFixMe: provided by @connect
-      nextProps.fetchList(nextProps.entityQuery, { reload: nextProps.reload });
+      this.fetchList(nextProps, { reload: nextProps.reload });
     } else if (this.props.loaded && !nextProps.loaded && !nextProps.loading) {
       // transitioned from loaded to not loaded, and isn't yet loading again
       // this typically means the list request state was cleared by a
       // create/update/delete action
-      // $FlowFixMe: provided by @connect
-      nextProps.fetchList(nextProps.entityQuery);
+      this.fetchList(nextProps);
     }
   }
 
@@ -127,8 +143,7 @@ export default class EntityListLoader extends React.Component {
   }
 
   reload = () => {
-    // $FlowFixMe: provided by @connect
-    return this.props.fetchList(this.props.entityQuery, { reload: true });
+    this.fetchList(this.props, { reload: true });
   };
 }
 
diff --git a/frontend/src/metabase/entities/index.js b/frontend/src/metabase/entities/index.js
index 2f67c01b8ea1e6833270f823cffa6ea158da04d6..ccf936380844b812cbbe9ce3cb0f66be9c95fcc3 100644
--- a/frontend/src/metabase/entities/index.js
+++ b/frontend/src/metabase/entities/index.js
@@ -9,6 +9,7 @@ export tables from "./tables";
 export fields from "./fields";
 export metrics from "./metrics";
 export segments from "./segments";
+export tasks from "./tasks";
 
 export users from "./users";
 
diff --git a/frontend/src/metabase/entities/tasks.js b/frontend/src/metabase/entities/tasks.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7684054577e5ded8e0774bba763b0f59ea80146
--- /dev/null
+++ b/frontend/src/metabase/entities/tasks.js
@@ -0,0 +1,6 @@
+import { createEntity } from "metabase/lib/entities";
+
+export default createEntity({
+  name: "tasks",
+  path: "/api/task",
+});
diff --git a/frontend/src/metabase/hoc/PaginationState.jsx b/frontend/src/metabase/hoc/PaginationState.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e85ff90b32f985f52eb1a6f964a515b06aa06c40
--- /dev/null
+++ b/frontend/src/metabase/hoc/PaginationState.jsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+const paginationState = () => ComposedComponent =>
+  class extends React.Component {
+    constructor(props) {
+      super(props);
+      this.state = {
+        page: props.initialPage || 0,
+        hasMorePages: null,
+      };
+    }
+    handleChangeHasMorePages = hasMorePages => {
+      this.setState({ hasMorePages });
+    };
+    handleNextPage = () => {
+      this.setState({ page: this.state.page + 1, hasMorePages: null });
+    };
+    handlePreviousPage = () => {
+      this.setState({ page: this.state.page - 1, hasMorePages: true });
+    };
+    render() {
+      const isPaginated = typeof this.props.pageSize === "number";
+      const extraProps = isPaginated
+        ? {
+            ...this.state,
+            onChangeHasMorePages: this.handleChangeHasMorePages,
+            onNextPage: this.state.hasMorePages ? this.handleNextPage : null,
+            onPreviousPage:
+              this.state.page > 0 ? this.handlePreviousPage : null,
+          }
+        : {};
+
+      return <ComposedComponent {...extraProps} {...this.props} />;
+    }
+  };
+
+export default paginationState;
diff --git a/frontend/src/metabase/lib/entities.js b/frontend/src/metabase/lib/entities.js
index c282cf436ea2ca8330323c47aa2fec0b4d147530..c4fc49bcc0cb1a4a43687f61139a577d8c60457a 100644
--- a/frontend/src/metabase/lib/entities.js
+++ b/frontend/src/metabase/lib/entities.js
@@ -344,11 +344,27 @@ export function createEntity(def: EntityDefinition): Entity {
           requestStatePath: getListStatePath(entityQuery),
           existingStatePath: getListStatePath(entityQuery),
           getData: async () => {
-            const { result, entities } = normalize(
-              await entity.api.list(entityQuery || {}),
-              [entity.schema],
-            );
-            return { result, entities, entityQuery };
+            const fetched = await entity.api.list(entityQuery || {});
+            let results = fetched;
+
+            // for now at least paginated endpoints have a 'data' property that
+            // contains the actual entries, if that is on the response we should
+            // use that as the 'results'
+            if (fetched.data) {
+              results = fetched.data;
+            }
+            const { result, entities } = normalize(results, [entity.schema]);
+            return {
+              result,
+              entities,
+              entityQuery,
+              // capture some extra details from the result just in case?
+              resultDetails: {
+                total: fetched.total,
+                offset: fetched.offset,
+                limit: fetched.limit,
+              },
+            };
           },
         }),
     ),
diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx
index 85c75f935cadbd2f3ffbe8faa54302ecd995174f..fa618569dabe5ee28866939728b53a50231b6f44 100644
--- a/frontend/src/metabase/nav/containers/Navbar.jsx
+++ b/frontend/src/metabase/nav/containers/Navbar.jsx
@@ -221,6 +221,11 @@ export default class Navbar extends Component {
               path="/admin/permissions"
               currentPath={this.props.path}
             />
+            <AdminNavItem
+              name={t`Troubleshooting`}
+              path="/admin/troubleshooting"
+              currentPath={this.props.path}
+            />
           </ul>
 
           <ProfileLink {...this.props} />
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index b614da645883e20150fda06ed5b1b121f69475c2..ddd8701ce4268b85348dca99570a69ca733f1012 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -312,6 +312,10 @@ export const I18NApi = {
   locale: GET("/app/locales/:locale.json"),
 };
 
+export const TaskApi = {
+  get: GET("api/task"),
+};
+
 export function setPublicQuestionEndpoints(uuid: string) {
   setFieldEndpoints("/api/public/card/:uuid", { uuid });
 }
diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj
index e5babb2903699e7fe24ab57aa422d204f78694fe..616d55cce3edaebd33f7493b1dd2ce317c32b4b1 100644
--- a/src/metabase/api/routes.clj
+++ b/src/metabase/api/routes.clj
@@ -31,6 +31,7 @@
              [slack :as slack]
              [table :as table]
              [tiles :as tiles]
+             [task :as task]
              [user :as user]
              [util :as util]]
             [metabase.middleware :as middleware]
@@ -82,6 +83,7 @@
   (context "/setup"                [] setup/routes)
   (context "/slack"                [] (+auth slack/routes))
   (context "/table"                [] (+auth table/routes))
+  (context "/task"                 [] (+auth task/routes))
   (context "/tiles"                [] (+auth tiles/routes))
   (context "/user"                 [] (+auth user/routes))
   (context "/util"                 [] util/routes)
diff --git a/src/metabase/api/task.clj b/src/metabase/api/task.clj
new file mode 100644
index 0000000000000000000000000000000000000000..4f40cf95e31a5b7fa5c62ac490698d2d95fb62a9
--- /dev/null
+++ b/src/metabase/api/task.clj
@@ -0,0 +1,44 @@
+(ns metabase.api.task
+  "/api/task endpoints"
+  (:require [compojure.core :refer [GET]]
+            [metabase.api.common :as api]
+            [metabase.models.task-history :as task-history :refer [TaskHistory]]
+            [metabase.util
+             [i18n :as ui18n :refer [tru]]
+             [schema :as su]]
+            [schema.core :as s]
+            [toucan.db :as db]))
+
+(defn- check-valid-limit [limit offset]
+  (when (and offset (not limit))
+    (throw
+     (ui18n/ex-info (tru "When including an offset, a limit must also be included.")
+       {:status-code 400}))))
+
+(defn- check-valid-offset [limit offset]
+  (when (and limit (not offset))
+    (throw
+     (ui18n/ex-info (tru "When including a limit, an offset must also be included.")
+       {:status-code 400}))))
+
+(api/defendpoint GET "/"
+  "Fetch a list of recent tasks stored as Task History"
+  [limit offset]
+  {limit  (s/maybe su/IntStringGreaterThanZero)
+   offset (s/maybe su/IntStringGreaterThanOrEqualToZero)}
+  (api/check-superuser)
+  (check-valid-limit limit offset)
+  (check-valid-offset limit offset)
+  (let [limit-int  (some-> limit Integer/parseInt)
+        offset-int (some-> offset Integer/parseInt)]
+    {:total  (db/count TaskHistory)
+     :limit  limit-int
+     :offset offset-int
+     :data   (task-history/all limit-int offset-int)}))
+
+(api/defendpoint GET "/:id"
+  "Get `TaskHistory` entry with ID."
+  [id]
+  (api/read-check TaskHistory id))
+
+(api/define-routes)
diff --git a/src/metabase/models/task_history.clj b/src/metabase/models/task_history.clj
index a2257b13445f303895dd9b09ae80a596b6449a12..0b699dc4199e2b61a0813539334d491b0363c3a6 100644
--- a/src/metabase/models/task_history.clj
+++ b/src/metabase/models/task_history.clj
@@ -1,6 +1,8 @@
 (ns metabase.models.task-history
   (:require [metabase.models.interface :as i]
             [metabase.util :as u]
+            [metabase.util.schema :as su]
+            [schema.core :as s]
             [toucan
              [db :as db]
              [models :as models]]))
@@ -29,3 +31,13 @@
   (merge i/IObjectPermissionsDefaults
          {:can-read?  i/superuser?
           :can-write? i/superuser?}))
+
+(s/defn all
+  "Return all TaskHistory entries, applying `limit` and `offset` if not nil"
+  [limit :- (s/maybe su/IntGreaterThanZero)
+   offset :- (s/maybe su/IntGreaterThanOrEqualToZero)]
+  (db/select TaskHistory (merge {:order-by [[:ended_at :desc]]}
+                                (when limit
+                                  {:limit limit})
+                                (when offset
+                                  {:offset offset}))))
diff --git a/src/metabase/util/schema.clj b/src/metabase/util/schema.clj
index b6f46b63be121122d72749bfbb19ffb54d723e9e..96c17e45100194cf1d59a1d87f0f54e59d713524 100644
--- a/src/metabase/util/schema.clj
+++ b/src/metabase/util/schema.clj
@@ -203,6 +203,12 @@
   (with-api-error-message (s/constrained s/Str #(u/ignore-exceptions (< 0 (Integer/parseInt %))))
     (tru "value must be a valid integer greater than zero.")))
 
+(def IntStringGreaterThanOrEqualToZero
+  "Schema for a string that can be parsed as an integer, and is greater than or equal to zero.
+   Something that adheres to this schema is guaranteed to to work with `Integer/parseInt`."
+  (with-api-error-message (s/constrained s/Str #(u/ignore-exceptions (<= 0 (Integer/parseInt %))))
+    (tru "value must be a valid integer greater than or equal to zero.")))
+
 (defn- boolean-string? ^Boolean [s]
   (boolean (when (string? s)
              (let [s (str/lower-case s)]
diff --git a/test/metabase/api/task_test.clj b/test/metabase/api/task_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..2b9aed04ccefd3ce1b54c62ca99f08f0babec9e9
--- /dev/null
+++ b/test/metabase/api/task_test.clj
@@ -0,0 +1,119 @@
+(ns metabase.api.task-test
+  (:require [clj-time.core :as time]
+            [expectations :refer :all]
+            [metabase.models.task-history :refer [TaskHistory]]
+            [metabase.test.data.users :as users]
+            [metabase.test.util :as tu]
+            [metabase.util :as u]
+            [metabase.util.date :as du]
+            [toucan.db :as db]
+            [toucan.util.test :as tt]))
+
+(def ^:private default-task-history
+  {:id true, :db_id true, :started_at true, :ended_at true, :duration 10, :task_details nil})
+
+(defn- generate-tasks
+  "Creates `n` task history maps with guaranteed increasing `:ended_at` times. This means that when stored and queried
+  via the GET `/` endpoint, will return in reverse order from how this function returns the task history maps."
+  [n]
+  (let [task-names (repeatedly n tu/random-name)
+        now        (time/now)]
+    (map-indexed (fn [idx task-name]
+                   {:task       task-name
+                    :started_at (du/->Timestamp now)
+                    :ended_at   (du/->Timestamp (time/plus now (time/seconds idx)))})
+                 task-names)))
+
+;; Only superusers can query for TaskHistory
+(expect
+  "You don't have permissions to do that."
+  ((users/user->client :rasta) :get 403 "task/"))
+
+;; Superusers can query TaskHistory, should return DB results
+(let [[task-hist-1 task-hist-2] (generate-tasks 2)
+      task-hist-1 (assoc task-hist-1 :duration 100)
+      task-hist-2 (assoc task-hist-1 :duration 200 :task_details {:some "complex", :data "here"})
+      task-names (set (map :task [task-hist-1 task-hist-2]))]
+  (expect
+    (set (map (fn [task-hist]
+                (merge default-task-history (select-keys task-hist [:task :duration :task_details])))
+              [task-hist-2 task-hist-1]))
+    (tt/with-temp* [TaskHistory [task-1 task-hist-1]
+                    TaskHistory [task-2 task-hist-2]]
+      (set (for [result (:data ((users/user->client :crowberto) :get 200 "task/"))
+                 :when  (contains? task-names (:task result))]
+             (tu/boolean-ids-and-timestamps result))))))
+
+;; Multiple results should be sorted via `:ended_at`. Below creates two tasks, the second one has a later `:ended_at`
+;; date and should be returned first
+(let [[task-hist-1 task-hist-2 :as task-histories] (generate-tasks 2)
+      task-names (set (map :task task-histories))]
+  (expect
+    (map (fn [{:keys [task]}]
+           (assoc default-task-history :task task))
+         [task-hist-2 task-hist-1])
+    (tt/with-temp* [TaskHistory [task-1 task-hist-1]
+                    TaskHistory [task-2 task-hist-2]]
+      (for [result (:data ((users/user->client :crowberto) :get 200 "task/"))
+            :when  (contains? task-names (:task result))]
+        (tu/boolean-ids-and-timestamps result)))))
+
+;; Should fail when only including a limit
+(expect
+  "When including a limit, an offset must also be included."
+  ((users/user->client :crowberto) :get 400 "task/" :limit 100))
+
+;; Should fail when only including an offset
+(expect
+  "When including an offset, a limit must also be included."
+  ((users/user->client :crowberto) :get 400 "task/" :offset 100))
+
+;; Check that we don't support a 0 limit, which wouldn't make sense
+(expect
+  {:errors {:limit "value may be nil, or if non-nil, value must be a valid integer greater than zero."}}
+  ((users/user->client :crowberto) :get 400 "task/" :limit 0 :offset 100))
+
+;; Check that paging information is applied when provided and included in the response
+(let [[task-hist-1 task-hist-2 task-hist-3 task-hist-4] (generate-tasks 4)]
+  (expect
+    [{:total 4, :limit 2, :offset 0
+      :data     (map (fn [{:keys [task]}]
+                       (assoc default-task-history :task task))
+                     [task-hist-4 task-hist-3])}
+     {:total 4, :limit 2, :offset 2
+      :data (map (fn [{:keys [task]}]
+                       (assoc default-task-history :task task))
+                     [task-hist-2 task-hist-1])}]
+    (do
+      (db/delete! TaskHistory)
+      (tt/with-temp* [TaskHistory [task-1 task-hist-1]
+                      TaskHistory [task-2 task-hist-2]
+                      TaskHistory [task-3 task-hist-3]
+                      TaskHistory [task-4 task-hist-4]]
+        (map tu/boolean-ids-and-timestamps
+             [((users/user->client :crowberto) :get 200 "task/" :limit 2 :offset 0)
+              ((users/user->client :crowberto) :get 200 "task/" :limit 2 :offset 2)])))))
+
+;; Only superusers can query for TaskHistory
+(expect
+  "You don't have permissions to do that."
+  ((users/user->client :rasta) :get 403 "task/"))
+
+;; Regular users can't query for a specific TaskHistory
+(expect
+  "You don't have permissions to do that."
+  (tt/with-temp TaskHistory [task]
+    ((users/user->client :rasta) :get 403 (format "task/%s" (u/get-id task)))))
+
+;; Superusers querying for a TaskHistory that doesn't exist will get a 404
+(expect
+  "Not found."
+  ((users/user->client :crowberto) :get 404 (format "task/%s" Integer/MAX_VALUE)))
+
+;; Superusers querying for specific TaskHistory will get that task info
+(expect
+  (merge default-task-history {:task "Test Task", :duration 100})
+  (tt/with-temp TaskHistory [task {:task "Test Task"
+                                   :duration 100}]
+    (tu/boolean-ids-and-timestamps
+     ((users/user->client :crowberto) :get 200 (format "task/%s" (u/get-id task))))))
diff --git a/yarn.lock b/yarn.lock
index f9ead6746ad6e7820ab1cec06894c2081e6c723f..6132985205478ea3100e74a9293fa2e4dd5a5e97 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3713,11 +3713,6 @@ dom-serializer@0, dom-serializer@~0.1.0:
     domelementtype "~1.1.1"
     entities "~1.1.1"
 
-dom-walk@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
-  integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=
-
 domain-browser@^1.1.1:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
@@ -5197,14 +5192,6 @@ global-dirs@^0.1.0:
   dependencies:
     ini "^1.3.4"
 
-global@^4.3.0:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
-  integrity sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=
-  dependencies:
-    min-document "^2.19.0"
-    process "~0.5.1"
-
 globals-docs@^2.3.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/globals-docs/-/globals-docs-2.4.0.tgz#f2c647544eb6161c7c38452808e16e693c2dafbb"
@@ -8088,13 +8075,6 @@ mimic-fn@^1.0.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
   integrity sha1-5md4PZLonb00KBi1IwudYqZyrRg=
 
-min-document@^2.19.0:
-  version "2.19.0"
-  resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
-  integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
-  dependencies:
-    dom-walk "^0.1.0"
-
 minimalistic-assert@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
@@ -10161,11 +10141,6 @@ process@^0.11.10:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
 
-process@~0.5.1:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
-  integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=
-
 progress@^1.1.8:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
@@ -13798,14 +13773,6 @@ xdg-basedir@^3.0.0:
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
   integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
 
-xhr-mock@^2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-2.4.1.tgz#cb502e3d50b8b2ec31bd61766ce516bfc1dd072f"
-  integrity sha1-y1AuPVC4suwxvWF2bOUWv8HdBy8=
-  dependencies:
-    global "^4.3.0"
-    url "^0.11.0"
-
 xml-char-classes@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"