Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.3] [com_scheduler] task execution history view #3350

Open
jgerman-bot opened this issue Dec 7, 2024 · 0 comments · May be fixed by #3351
Open

[5.3] [com_scheduler] task execution history view #3350

jgerman-bot opened this issue Dec 7, 2024 · 0 comments · May be fixed by #3351

Comments

@jgerman-bot
Copy link

New language relevant PR in upstream repo: joomla/joomla-cms#42530 Here are the upstream changes:

Click to expand the diff!
diff --git a/administrator/components/com_admin/sql/updates/mysql/5.3.0-2024-10-26.sql b/administrator/components/com_admin/sql/updates/mysql/5.3.0-2024-10-26.sql
new file mode 100644
index 0000000000000..b2e6c94cd0fcf
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/mysql/5.3.0-2024-10-26.sql
@@ -0,0 +1,20 @@
+--
+-- Table structure for table `#__scheduler_logs`
+--
+
+CREATE TABLE IF NOT EXISTS `#__scheduler_logs` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `taskname` varchar(255) NOT NULL DEFAULT '',
+  `tasktype` varchar(128) NOT NULL COMMENT 'unique identifier for job defined by plugin',
+  `duration` DECIMAL(5,3) NOT NULL,
+  `jobid` int UNSIGNED NOT NULL,
+  `taskid` int UNSIGNED NOT NULL,
+  `exitcode` int NOT NULL,
+  `lastdate` datetime COMMENT 'Timestamp of last run',
+  `nextdate` datetime COMMENT 'Timestamp of next (planned) run, referred for execution on trigger',
+  PRIMARY KEY (id),
+  KEY `idx_taskname` (`taskname`),
+  KEY `idx_tasktype` (`tasktype`),
+  KEY `idx_lastdate` (`lastdate`),
+  KEY `idx_nextdate` (`nextdate`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci;
diff --git a/administrator/components/com_admin/sql/updates/postgresql/5.3.0-2024-10-26.sql b/administrator/components/com_admin/sql/updates/postgresql/5.3.0-2024-10-26.sql
new file mode 100644
index 0000000000000..49794391fb4c4
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/postgresql/5.3.0-2024-10-26.sql
@@ -0,0 +1,20 @@
+--
+-- Table structure for table "#__scheduler_logs"
+--
+
+CREATE TABLE IF NOT EXISTS "#__scheduler_logs" (
+  "id" serial NOT NULL,
+  "taskname" varchar(255) DEFAULT '' NOT NULL,
+  "tasktype" varchar(128) NOT NULL,
+  "duration" NUMERIC(5,3) NOT NULL,
+  "jobid" integer NOT NULL,
+  "taskid" integer NOT NULL,
+  "exitcode" integer NOT NULL,
+  "lastdate" timestamp without time zone,
+  "nextdate" timestamp without time zone,
+  PRIMARY KEY (id)
+);
+CREATE INDEX "#__scheduler_logs_idx_taskname" ON "#__scheduler_logs" ("taskname");
+CREATE INDEX "#__scheduler_logs_idx_tasktype" ON "#__scheduler_logs" ("tasktype");
+CREATE INDEX "#__scheduler_logs_idx_lastdate" ON "#__scheduler_logs" ("lastdate");
+CREATE INDEX "#__scheduler_logs_idx_nextdate" ON "#__scheduler_logs" ("nextdate");
diff --git a/administrator/components/com_scheduler/forms/filter_logs.xml b/administrator/components/com_scheduler/forms/filter_logs.xml
new file mode 100644
index 0000000000000..8fcfddc8e24f1
--- /dev/null
+++ b/administrator/components/com_scheduler/forms/filter_logs.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<form addfieldprefix="Joomla\Component\Scheduler\Administrator\Field">
+	<fields name="filter">
+		<field
+			name="search"
+			type="text"
+			label="COM_SCHEDULER_FILTER_SEARCH_LABEL"
+			description="COM_SCHEDULER_FILTER_SEARCH_HISTORY_DESC"
+			inputmode="search"
+			hint="JSEARCH_FILTER"
+		/>
+		<field
+			name="type"
+			type="taskType"
+			label="COM_SCHEDULER_HEADING_TASK_TYPE"
+			class="js-select-submit-on-change"
+			>
+			<option value="">COM_SCHEDULER_SELECT_TYPE</option>
+		</field>
+		<field
+			name="exitcode"
+			type="exitCode"
+			label="COM_SCHEDULER_EXITCODE"
+			class="js-select-submit-on-change"
+			>
+			<option value="">COM_SCHEDULER_FILTER_SELECT_OPTION_EXITCODE</option>
+		</field>
+	</fields>
+	<fields name="list">
+		<field
+			name="fullordering"
+			type="list"
+			label="JGLOBAL_SORT_BY"
+			class="js-select-submit-on-change"
+			default="a.id DESC"
+			validate="options"
+			>
+			<option value="">JGLOBAL_SORT_BY</option>
+			<option value="a.taskname ASC">JGLOBAL_TITLE_ASC</option>
+			<option value="a.taskname DESC">JGLOBAL_TITLE_DESC</option>
+			<option value="a.tasktype ASC">COM_SCHEDULER_TASK_TYPE_ASC</option>
+			<option value="a.tasktype DESC">COM_SCHEDULER_TASK_TYPE_DESC</option>
+			<option value="a.taskid ASC">COM_SCHEDULER_TASK_TIMES_ASC</option>
+			<option value="a.taskid DESC">COM_SCHEDULER_TASK_TIMES_DESC</option>
+			<option value="a.lastdate ASC">COM_SCHEDULER_LAST_RUN_ASC</option>
+			<option value="a.lastdate DESC">COM_SCHEDULER_LAST_RUN_DESC</option>
+			<option value="a.duration ASC">COM_SCHEDULER_DURATION_ASC</option>
+			<option value="a.duration DESC">COM_SCHEDULER_DURATION_DESC</option>
+			<option value="a.exitcode ASC">COM_SCHEDULER_EXIT_CODE_ASC</option>
+			<option value="a.exitcode DESC">COM_SCHEDULER_EXIT_CODE_DESC</option>
+			<option value="a.nextdate ASC">COM_SCHEDULER_NEXT_RUN_ASC</option>
+			<option value="a.nextdate DESC">COM_SCHEDULER_NEXT_RUN_DESC</option>
+			<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
+			<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
+		</field>
+		<field
+			name="limit"
+			type="limitbox"
+			label="JGLOBAL_LIST_LIMIT"
+			default="5"
+			class="js-select-submit-on-change"
+		/>
+	</fields>
+</form>
\ No newline at end of file
diff --git a/administrator/components/com_scheduler/src/Controller/LogsController.php b/administrator/components/com_scheduler/src/Controller/LogsController.php
new file mode 100644
index 0000000000000..41fcf97720531
--- /dev/null
+++ b/administrator/components/com_scheduler/src/Controller/LogsController.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_scheduler
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Scheduler\Administrator\Controller;
+
+use Joomla\CMS\Access\Exception\NotAllowed;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\MVC\Controller\AdminController;
+use Joomla\Utilities\ArrayHelper;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * Logs list controller class.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class LogsController extends AdminController
+{
+    /**
+     * The prefix to use with controller messages.
+     *
+     * @var    string
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected $text_prefix = 'COM_SCHEDULER_LOGS';
+
+    /**
+     * Proxy for getModel.
+     *
+     * @param   string  $name    The name of the model.
+     * @param   string  $prefix  The prefix for the PHP class name.
+     * @param   array   $config  Array of configuration parameters.
+     *
+     * @return  \Joomla\CMS\MVC\Model\BaseDatabaseModel
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function getModel($name = 'Logs', $prefix = 'Administrator', $config = ['ignore_request' => true])
+    {
+        return parent::getModel($name, $prefix, $config);
+    }
+
+    /**
+     * Clean out the logs.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function purge()
+    {
+        // Check for request forgeries.
+        $this->checkToken();
+
+        $model = $this->getModel('Logs');
+
+        if ($model->purge()) {
+            $message = Text::_('COM_SCHEDULER_LOGS_CLEAR');
+        } else {
+            $message = Text::_('COM_SCHEDULER_CLEAR_FAIL');
+        }
+
+        $this->setRedirect('index.php?option=com_scheduler&view=logs', $message);
+    }
+
+    /**
+     * Removes an item.
+     *
+     * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function delete(): void
+    {
+        $ids = $this->input->get('cid', [], 'array');
+
+        if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
+            throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
+        }
+
+        if (empty($ids)) {
+            $this->setMessage(Text::_('COM_SCHEDULER_NO_LOGS_SELECTED'), 'warning');
+            $this->setRedirect('index.php?option=com_scheduler&view=logs');
+            return;
+        }
+
+        // Get the model.
+        $model = $this->getModel();
+        $ids   = ArrayHelper::toInteger($ids);
+
+        // Remove the items.
+        if ($model->delete($ids)) {
+            $this->setMessage(Text::plural('COM_SCHEDULER_N_ITEMS_DELETED', \count($ids)));
+        }
+
+        $this->setRedirect('index.php?option=com_scheduler&view=logs');
+    }
+}
diff --git a/administrator/components/com_scheduler/src/Field/ExitCodeField.php b/administrator/components/com_scheduler/src/Field/ExitCodeField.php
new file mode 100644
index 0000000000000..58867d1884e3c
--- /dev/null
+++ b/administrator/components/com_scheduler/src/Field/ExitCodeField.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_scheduler
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Scheduler\Administrator\Field;
+
+use Joomla\CMS\Form\Field\PredefinedlistField;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * A predefined list field with all possible states for a com_scheduler entry.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class ExitCodeField extends PredefinedlistField
+{
+    /**
+     * The form field type.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    public $type = 'exitCode';
+
+    /**
+     * Available states
+     *
+     * @var  string[]
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $predefinedOptions = [
+        5   => 'COM_SCHEDULER_EXIT_CODE_FAILED',
+        0   => 'COM_SCHEDULER_EXIT_CODE_EXECUTED',
+        123 => 'COM_SCHEDULER_EXIT_CODE_WILLRESUME',
+        '*' => 'JALL',
+    ];
+}
diff --git a/administrator/components/com_scheduler/src/Model/LogsModel.php b/administrator/components/com_scheduler/src/Model/LogsModel.php
new file mode 100644
index 0000000000000..979bf17b4d13b
--- /dev/null
+++ b/administrator/components/com_scheduler/src/Model/LogsModel.php
@@ -0,0 +1,275 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_scheduler
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Scheduler\Administrator\Model;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+use Joomla\CMS\Component\ComponentHelper;
+use Joomla\CMS\Factory;
+use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
+use Joomla\CMS\MVC\Model\ListModel;
+use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
+use Joomla\Database\ParameterType;
+
+/**
+ * Supporting a list of logs.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class LogsModel extends ListModel
+{
+    /**
+     * Constructor.
+     *
+     * @param   array                $config   An optional associative array of configuration settings.
+     * @param   MVCFactoryInterface  $factory  The factory.
+     *
+     * @see     \JControllerLegacy
+     * @since   __DEPLOY_VERSION__
+     */
+    public function __construct($config = [], ?MVCFactoryInterface $factory = null)
+    {
+        if (empty($config['filter_fields'])) {
+            $config['filter_fields'] = [
+                'id', 'a.id',
+                'exitcode', 'a.exitcode',
+                'duration', 'a.duration',
+                'taskname', 'a.taskname',
+                'type', 'a.tasktype',
+                'jobid', 'a.jobid',
+                'taskid', 'a.taskid',
+                'lastdate', 'a.lastdate',
+                'nextdate', 'a.nextdate',
+            ];
+        }
+
+        parent::__construct($config, $factory);
+    }
+
+    /**
+     * Removes all the logs from the table.
+     *
+     * @return  boolean result of operation
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function purge()
+    {
+        try {
+            $this->getDatabase()->truncateTable('#__scheduler_logs');
+        } catch (\Exception $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Auto-populate the model state.
+     *
+     * Note. Calling getState in this method will result in recursion.
+     *
+     * @param   string  $ordering   An optional ordering field.
+     * @param   string  $direction  An optional direction (asc|desc).
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function populateState($ordering = 'a.id', $direction = 'desc')
+    {
+        // Load the parameters.
+        $params = ComponentHelper::getParams('com_scheduler');
+        $this->setState('params', $params);
+
+        // List state information.
+        parent::populateState($ordering, $direction);
+    }
+
+    /**
+     * Get a store id based on model configuration state.
+     *
+     * This is necessary because the model is used by the component and
+     * different modules that might need different sets of data or different
+     * ordering requirements.
+     *
+     * @param   string  $id  A prefix for the store id.
+     *
+     * @return  string  A store id.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getStoreId($id = '')
+    {
+        // Compile the store id.
+        $id .= ':' . $this->getState('filter.search');
+        $id .= ':' . $this->getState('filter.state');
+        $id .= ':' . $this->getState('filter.exitcode');
+        $id .= ':' . $this->getState('filter.type');
+
+        return parent::getStoreId($id);
+    }
+
+    /**
+     * Build an SQL query to load the list data.
+     *
+     * @return  \JDatabaseQuery
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getListQuery()
+    {
+        // Create a new query object.
+        $db    = $this->getDatabase();
+        $query = $db->getQuery(true);
+
+        // Select the required fields from the table.
+        $query->select(
+            $this->getState(
+                'list.select',
+                'a.*'
+            )
+        );
+        $query->from($db->quoteName('#__scheduler_logs', 'a'));
+
+        // Filter the items over the exit code.
+        $exitCode = $this->getState('filter.exitcode');
+
+        if (is_numeric($exitCode)) {
+            $exitCode = (int) $exitCode;
+            if ($exitCode >= 0) {
+                $query->where($db->quoteName('a.exitcode') . ' = :exitcode')
+                    ->bind(':exitcode', $exitCode, ParameterType::INTEGER);
+            } else {
+                $query->whereNotIn($db->quoteName('a.exitcode'), [0, 123], ParameterType::INTEGER);
+            }
+        }
+
+        // Filter the items over the search string if set.
+        $search = $this->getState('filter.search');
+
+        if (!empty($search)) {
+            if (stripos($search, 'id:') === 0) {
+                $ids = (int) substr($search, 3);
+                $query->where($db->quoteName('a.id') . ' = :id');
+                $query->bind(':id', $ids, ParameterType::INTEGER);
+            } else {
+                $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%');
+                $query->where($db->quoteName('taskname') . ' LIKE :taskname')
+                    ->bind(':taskname', $search);
+            }
+        }
+
+        // Filter over type
+        $typeFilter = $this->getState('filter.type');
+
+        if ($typeFilter) {
+            $taskOptions   = SchedulerHelper::getTaskOptions();
+            $safeTypeTitle = $taskOptions->findOption($typeFilter)->title ?? '';
+            $query->where($db->quotename('a.tasktype') . ' = :type')
+                ->bind(':type', $safeTypeTitle);
+        }
+
+        // Add the list ordering clause.
+        $query->order($db->escape($this->getState('list.ordering', 'a.lastdate')) . ' ' . $db->escape($this->getState('list.direction', 'DESC')));
+
+        return $query;
+    }
+
+    /**
+     * Delete rows.
+     *
+     * @param   array    $pks    The ids of the items to delete.
+     *
+     * @return  boolean  Returns true on success, false on failure.
+     */
+    public function delete($pks)
+    {
+        if ($this->canDelete($pks)) {
+            // Delete logs from list
+            $db    = $this->getDatabase();
+            $query = $db->getQuery(true)
+                ->delete($db->quoteName('#__scheduler_logs'))
+                ->whereIn($db->quoteName('id'), $pks);
+
+            $db->setQuery($query);
+            $this->setError((string) $query);
+
+            try {
+                $db->execute();
+            } catch (\RuntimeException $e) {
+                $this->setError($e->getMessage());
+
+                return false;
+            }
+        } else {
+            Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
+        }
+
+        return true;
+    }
+
+    /**
+     * Determine whether a record may be deleted taking into consideration
+     * the user's permissions over the record.
+     *
+     * @param   object  $record  The database row/record in question
+     *
+     * @return  boolean  True if the record may be deleted
+     *
+     * @since  __DEPLOY_VERSION__
+     * @throws \Exception
+     */
+    protected function canDelete($record): bool
+    {
+        // Record doesn't exist, can't delete
+        if (empty($record)) {
+            return false;
+        }
+
+        return Factory::getApplication()->getIdentity()->authorise('core.delete', 'com_scheduler');
+    }
+
+    /**
+     * @param   array   $data        The task execution data.
+     *
+     * @return void
+     *
+     * @since __DEPLOY_VERSION__
+     * @throws Exception
+     */
+    public function logTask(array $data): void
+    {
+        $model         = Factory::getApplication()->bootComponent('com_scheduler')
+            ->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]);
+        $taskInfo      = $model->getItem($data['TASK_ID']);
+        $taskOptions   = SchedulerHelper::getTaskOptions();
+        $safeTypeTitle = $taskOptions->findOption($taskInfo->type)->title ?? '';
+        $duration      = ($data['TASK_DURATION'] ?? 0);
+        $created       = Factory::getDate()->toSql();
+
+        /** @var \Joomla\Component\Schduler\Administrator\Table\LogsTable $table */
+        $logsTable           = $this->getTable();
+        $logsTable->tasktype = $safeTypeTitle;
+        $logsTable->taskname = $data['TASK_TITLE'];
+        $logsTable->duration = $duration;
+        $logsTable->jobid    = $data['TASK_ID'];
+        $logsTable->exitcode = $data['EXIT_CODE'];
+        $logsTable->taskid   = $data['TASK_TIMES'];
+        $logsTable->lastdate = $created;
+        $logsTable->nextdate = $taskInfo->next_execution;
+
+        // Log the execution of the task.
+        $logsTable->store();
+    }
+}
diff --git a/administrator/components/com_scheduler/src/Table/LogsTable.php b/administrator/components/com_scheduler/src/Table/LogsTable.php
new file mode 100644
index 0000000000000..78ec38c5345ca
--- /dev/null
+++ b/administrator/components/com_scheduler/src/Table/LogsTable.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_scheduler
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Scheduler\Administrator\Table;
+
+use Joomla\CMS\Table\Table;
+use Joomla\Database\DatabaseDriver;
+use Joomla\Event\DispatcherInterface;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * Logs Table class.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class LogsTable extends Table
+{
+    /**
+     * Constructor
+     *
+     * @param   DatabaseDriver        $db          Database connector object
+     * @param   ?DispatcherInterface  $dispatcher  Event dispatcher for this table
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function __construct(DatabaseDriver $db, ?DispatcherInterface $dispatcher = null)
+    {
+        parent::__construct('#__scheduler_logs', 'id', $db, $dispatcher);
+    }
+}
diff --git a/administrator/components/com_scheduler/src/View/Logs/HtmlView.php b/administrator/components/com_scheduler/src/View/Logs/HtmlView.php
new file mode 100644
index 0000000000000..083d4382c180b
--- /dev/null
+++ b/administrator/components/com_scheduler/src/View/Logs/HtmlView.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_scheduler
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Scheduler\Administrator\View\Logs;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Form\Form;
+use Joomla\CMS\Helper\ContentHelper;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\MVC\View\GenericDataException;
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Pagination\Pagination;
+use Joomla\CMS\Toolbar\Toolbar;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+use Joomla\Registry\Registry;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * View class for a list of logs.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class HtmlView extends BaseHtmlView
+{
+    /**
+     * The search tools form
+     *
+     * @var    Form
+     * @since  __DEPLOY_VERSION__
+     */
+    public $filterForm;
+
+    /**
+     * The active search filters
+     *
+     * @var    array
+     * @since  __DEPLOY_VERSION__
+     */
+    public $activeFilters = [];
+
+    /**
+     * An array of items
+     *
+     * @var    array
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $items = [];
+
+    /**
+     * The pagination object
+     *
+     * @var    Pagination
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $pagination;
+
+    /**
+     * The model state
+     *
+     * @var    Registry
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $state;
+
+    /**
+     * Method to display the view.
+     *
+     * @param   string  $tpl  A template file to load. [optional]
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  \Exception
+     */
+    public function display($tpl = null): void
+    {
+        /** @var LogsModel $model */
+        $model               = $this->getModel();
+        $this->items         = $model->getItems();
+        $this->pagination    = $model->getPagination();
+        $this->state         = $model->getState();
+        $this->filterForm    = $model->getFilterForm();
+        $this->activeFilters = $model->getActiveFilters();
+
+        // Check for errors.
+        if (\count($errors = $this->get('Errors'))) {
+            throw new GenericDataException(implode("\n", $errors), 500);
+        }
+
+        $this->addToolbar();
+        parent::display($tpl);
+    }
+
+    /**
+     * Add the page title and toolbar.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function addToolbar(): void
+    {
+        $canDo   = ContentHelper::getActions('com_scheduler');
+        $user    = Factory::getApplication()->getIdentity();
+        $toolbar = Toolbar::getInstance();
+
+        ToolbarHelper::title(Text::_('COM_SCHEDULER_FIELDSET_EXEC_HIST'), 'list');
+
+        $arrow   = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
+        $toolbar->link('JTOOLBAR_BACK', 'index.php?option=com_scheduler')
+            ->icon('icon-' . $arrow);
+
+        if ($canDo->get('core.delete') && \count($this->items)) {
+            $toolbar->delete('logs.delete')
+                ->message('JGLOBAL_CONFIRM_DELETE')
+                ->listCheck(true);
+
+            $toolbar->confirmButton('trash', 'COM_SCHEDULER_TOOLBAR_PURGE', 'logs.purge')
+                ->message('COM_SCHEDULER_TOOLBAR_PURGE_CONFIRM')
+                ->listCheck(false);
+        }
+
+        // Link to component preferences if user has admin privileges
+        if ($canDo->get('core.admin') || $canDo->get('core.options')) {
+            $toolbar->preferences('com_scheduler');
+        }
+
+        $toolbar->help('Scheduled_Tasks');
+    }
+}
diff --git a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php
index 169ffbd258ce1..d58d60c9f9552 100644
--- a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php
+++ b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php
@@ -168,6 +168,11 @@ protected function addToolbar(): void
             }
         }
 
+        $toolbar->linkButton('history', 'COM_SCHEDULER_EXECUTION_HISTORY')
+            ->url('index.php?option=com_scheduler&view=logs&layout=default')
+            ->buttonClass('btn btn-info')
+            ->icon('icon-menu');
+
         // Add "Empty Trash" button if filtering by trashed.
         if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) {
             $toolbar->delete('tasks.delete', 'JTOOLBAR_DELETE_FROM_TRASH')
diff --git a/administrator/components/com_scheduler/tmpl/logs/default.php b/administrator/components/com_scheduler/tmpl/logs/default.php
new file mode 100644
index 0000000000000..d9436aeb49a2a
--- /dev/null
+++ b/administrator/components/com_scheduler/tmpl/logs/default.php
@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_scheduler
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\HTML\HTMLHelper;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Layout\LayoutHelper;
+use Joomla\CMS\Router\Route;
+use Joomla\CMS\Uri\Uri;
+
+/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
+$wa = $this->document->getWebAssetManager();
+$wa->useScript('table.columns')
+    ->useScript('multiselect');
+
+$user      = $this->getCurrentUser();
+$listOrder = $this->escape($this->state->get('list.ordering'));
+$listDirn  = $this->escape($this->state->get('list.direction'));
+$canEdit   = $user->authorise('core.edit', 'com_scheduler');
+$canChange = $user->authorise('core.edit.state', 'com_scheduler');
+?>
+<form action="<?php echo Route::_('index.php?option=com_scheduler&view=logs'); ?>" method="post" name="adminForm" id="adminForm">
+    <div class="row">
+        <div class="col-md-12">
+            <div id="j-main-container" class="j-main-container">
+                <?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
+                <?php if (empty($this->items)) : ?>
+                    <div class="alert alert-info">
+                        <span class="icon-info-circle" aria-hidden="true"></span><span class="sr-only"><?php echo Text::_('INFO'); ?></span>
+                        <?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
+                    </div>
+                <?php else : ?>
+                    <table class="table itemList">
+                        <caption id="captionTable" class="visually-hidden">
+                            <?php echo Text::_('COM_SCHEDULER_TABLE_CAPTION'); ?>,
+                            <span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>,
+                            <span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span>
+                        </caption>
+                        <thead>
+                            <tr>
+                                <td class="w-1 text-center">
+                                    <?php echo HTMLHelper::_('grid.checkall'); ?>
+                                </td>
+                                <th scope="col" style="min-width:100px">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'JGLOBAL_TITLE', 'a.taskname', $listDirn, $listOrder); ?>
+                                </th>
+                                <!-- Task type header -->
+                                <th scope="col" class="w-1 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_TASK_TYPE', 'a.tasktype', $listDirn, $listOrder) ?>
+                                </th>
+                                <th scope="col" class="w-5 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_TIMES_EXEC', 'a.taskid', $listDirn, $listOrder); ?>
+                                </th>
+                                <th scope="col" class="w-5 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LAST_RUN_DATE', 'a.lastdate', $listDirn, $listOrder); ?>
+                                </th>
+                                <th scope="col" class="w-1 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_DURATION', 'a.duration', $listDirn, $listOrder); ?>
+                                </th>
+                                <th scope="col" class="w-5 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_EXIT_CODE', 'a.exitcode', $listDirn, $listOrder); ?>
+                                </th>
+                                <th scope="col" class="w-5 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'COM_SCHEDULER_LABEL_NEXT_EXEC', 'a.nextdate', $listDirn, $listOrder); ?>
+                                </th>
+                                <th scope="col" class="w-1 d-none d-md-table-cell">
+                                    <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <?php foreach ($this->items as $i => $item) : ?>
+                                <tr class="row<?php echo $i % 2; ?>">
+                                    <td class="text-center">
+                                        <?php echo HTMLHelper::_('grid.id', $i, $item->id); ?>
+                                    </td>
+                                    <th scope="row">
+                                        <?php if ($canEdit) : ?>
+                                            <a href="<?php echo Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $item->jobid); ?>"
+                                                title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo $this->escape($item->taskname); ?>"> <?php echo $this->escape($item->taskname); ?>
+                                            </a>
+                                        <?php else : ?>
+                                            <?php echo $this->escape(str_replace(Uri::root(), '', rawurldecode($item->taskname))); ?>
+                                        <?php endif; ?>
+                                    </th>
+                                    <!-- Item type -->
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php echo $this->escape($item->tasktype); ?>
+                                    </td>
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php echo (int) $item->taskid; ?>
+                                    </td>
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php echo HTMLHelper::_('date.relative', $item->lastdate, Text::_('DATE_FORMAT_LC6')); ?>
+                                    </td>
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php echo $item->duration; ?>
+                                    </td>
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php
+                                        switch ($item->exitcode) {
+                                            case '123':
+                                                echo '<span class="badge bg-secondary">' . Text::sprintf('COM_SCHEDULER_TASK_RESUME', $item->exitcode) . '</span>';
+                                                break;
+                                            case '0':
+                                                echo '<span class="badge bg-success">' . Text::sprintf('COM_SCHEDULER_TASK_EXECUTED', $item->exitcode) . '</span>';
+                                                break;
+                                            default:
+                                                echo '<span class="badge bg-danger">' . Text::sprintf('COM_SCHEDULER_TASK_FAILED', $item->exitcode) . '</span>';
+                                                break;
+                                        }
+                                        ?>
+                                    </td>
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php echo HTMLHelper::_('date', $item->nextdate, Text::_('DATE_FORMAT_LC6')); ?>
+                                    </td>
+                                    <td class="small d-none d-md-table-cell">
+                                        <?php echo (int) $item->id; ?>
+                                    </td>
+                                </tr>
+                            <?php endforeach; ?>
+                        </tbody>
+                    </table>
+                    <?php // load the pagination.
+                    ?>
+                    <?php echo $this->pagination->getListFooter(); ?>
+                <?php endif; ?>
+                <input type="hidden" name="task" value="">
+                <input type="hidden" name="boxchecked" value="0">
+                <?php echo HTMLHelper::_('form.token'); ?>
+            </div>
+        </div>
+    </div>
+</form>
diff --git a/administrator/language/en-GB/com_scheduler.ini b/administrator/language/en-GB/com_scheduler.ini
index f5eee988a7c2c..eac54933d7d34 100644
--- a/administrator/language/en-GB/com_scheduler.ini
+++ b/administrator/language/en-GB/com_scheduler.ini
@@ -4,6 +4,7 @@
 ; Note : All ini files need to be saved as UTF-8
 
 COM_SCHEDULER="Scheduled Tasks"
+COM_SCHEDULER_CLEAR_FAIL="Failed to delete execution history logs."
 COM_SCHEDULER_CONFIGURATION="Scheduled Tasks Configuration"
 COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_DESC="Configure how site visits trigger Scheduled Tasks."
 COM_SCHEDULER_CONFIG_FIELDSET_LAZY_SCHEDULER_LABEL="Lazy Scheduler"
@@ -26,6 +27,8 @@ COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_DESC="Copy the link to your clipboard."
 COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_FAIL="Could not copy link."
 COM_SCHEDULER_CONFIG_WEBCRON_LINK_COPY_SUCCESS="Link copied."
 COM_SCHEDULER_DESCRIPTION_TASK_PRIORITY="Higher priority tasks can potentially block lower priority tasks."
+COM_SCHEDULER_DURATION_ASC="Duration ascending"
+COM_SCHEDULER_DURATION_DESC="Duration descending"
 COM_SCHEDULER_EDIT_TASK="Edit Task"
 COM_SCHEDULER_EMPTYSTATE_BUTTON_ADD="Add your first Task"
 COM_SCHEDULER_EMPTYSTATE_CONTENT="Tasks are actions on your website that are scheduled to occur at set times."
@@ -33,10 +36,17 @@ COM_SCHEDULER_EMPTYSTATE_TITLE="No Tasks have been created yet."
 COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW="You need to select a Task type first."
 COM_SCHEDULER_ERROR_INVALID_TASK_TYPE="Invalid Task Type"
 COM_SCHEDULER_EXECUTION_CRON_EXPRESSION="Cron Expression (Advanced)"
+COM_SCHEDULER_EXECUTION_HISTORY="Execution History"
 COM_SCHEDULER_EXECUTION_INTERVAL_DAYS="Interval, Days"
 COM_SCHEDULER_EXECUTION_INTERVAL_HOURS="Interval, Hours"
 COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES="Interval, Minutes"
 COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS="Interval, Months"
+COM_SCHEDULER_EXITCODE="Exit Code"
+COM_SCHEDULER_EXIT_CODE_ASC="Exit Code ascending"
+COM_SCHEDULER_EXIT_CODE_DESC="Exit Code descending"
+COM_SCHEDULER_EXIT_CODE_EXECUTED="Executed"
+COM_SCHEDULER_EXIT_CODE_FAILED="Failed"
+COM_SCHEDULER_EXIT_CODE_WILLRESUME="Task Will Resume"
 COM_SCHEDULER_FIELDSET_BASIC="Basic Fields"
 COM_SCHEDULER_FIELDSET_CRON_OPTIONS="Cron Match"
 COM_SCHEDULER_FIELDSET_EXEC_HIST="Execution History"
@@ -60,10 +70,13 @@ COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MINUTES="Minutes"
 COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_MONTHS="Months"
 COM_SCHEDULER_FIELD_TASK_TYPE="Type ID"
 COM_SCHEDULER_FILTER_SEARCH_DESC="Search in task title and note. Prefix with ID: to search for a task ID."
+COM_SCHEDULER_FILTER_SEARCH_HISTORY_DESC="Search in task title. Prefix with ID: to search for a history ID."
 COM_SCHEDULER_FILTER_SEARCH_LABEL="Search Tasks"
+COM_SCHEDULER_FILTER_SELECT_OPTION_EXITCODE="- Exit Code -"
 COM_SCHEDULER_FORM_TITLE_EDIT="Edit Task"
 COM_SCHEDULER_FORM_TITLE_NEW="New Task"
 COM_SCHEDULER_HEADING_TASK_TYPE="- Task Type -"
+COM_SCHEDULER_LABEL_DURATION="Duration"
 COM_SCHEDULER_LABEL_EXEC_DAY="Execution Day"
 COM_SCHEDULER_LABEL_EXEC_INTERVAL="Execution Interval"
 COM_SCHEDULER_LABEL_EXEC_TIME="Execution Time (UTC)"
@@ -80,6 +93,7 @@ COM_SCHEDULER_LABEL_TIMES_FAIL="Times Failed"
 COM_SCHEDULER_LAST_RUN_ASC="Last Run ascending"
 COM_SCHEDULER_LAST_RUN_DATE="Last Run Date"
 COM_SCHEDULER_LAST_RUN_DESC="Last Run descending"
+COM_SCHEDULER_LOGS_CLEAR="All execution history logs have been deleted."
 COM_SCHEDULER_MANAGER_TASKS="Scheduled Tasks"
 COM_SCHEDULER_MANAGER_TASK_EDIT="Edit Task"
 COM_SCHEDULER_MANAGER_TASK_NEW="New Task"
@@ -105,6 +119,7 @@ COM_SCHEDULER_N_ITEMS_UNPUBLISHED="%d tasks disabled."
 COM_SCHEDULER_N_ITEMS_UNPUBLISHED_1="Task disabled."
 COM_SCHEDULER_N_ITEMS_UNLOCKED="%d tasks unlocked."
 COM_SCHEDULER_N_ITEMS_UNLOCKED_1="Task unlocked."
+COM_SCHEDULER_NO_LOGS_SELECTED="No execution history logs selected."
 COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL="Manual Execution"
 COM_SCHEDULER_OPTION_ORPHANED_HIDE="Hide Orphaned"
 COM_SCHEDULER_OPTION_ORPHANED_ONLY="Only Orphaned"
@@ -127,11 +142,16 @@ COM_SCHEDULER_TABLE_CAPTION="Tasks"
 COM_SCHEDULER_TASK="Task"
 COM_SCHEDULER_TASKS_VIEW_DEFAULT_DESC="Schedule and Manage Task Routines."
 COM_SCHEDULER_TASKS_VIEW_DEFAULT_TITLE="Scheduled Tasks"
+COM_SCHEDULER_TASK_EXECUTED="Executed: %s"
+COM_SCHEDULER_TASK_FAILED="Failed: %s"
 COM_SCHEDULER_TASK_PARAMS_FIELDSET_LABEL="Task Parameters"
 COM_SCHEDULER_TASK_PRIORITY="Task Priority"
 COM_SCHEDULER_TASK_PRIORITY_ASC="Task Priority ascending"
 COM_SCHEDULER_TASK_PRIORITY_DESC="Task Priority descending"
+COM_SCHEDULER_TASK_RESUME="Will Resume: %s"
 COM_SCHEDULER_TASK_ROUTINE_EXCEPTION="Routine threw exception: %1$s"
+COM_SCHEDULER_TASK_TIMES_ASC="Times ascending"
+COM_SCHEDULER_TASK_TIMES_DESC="Times descending"
 COM_SCHEDULER_TASK_TYPE="Task Type"
 COM_SCHEDULER_TASK_TYPE_ASC="Task Type ascending"
 COM_SCHEDULER_TASK_TYPE_DESC="Task Type descending"
@@ -144,6 +164,8 @@ COM_SCHEDULER_TEST_RUN_STATUS_TERMINATED="Status: Terminated"
 COM_SCHEDULER_TEST_RUN_TASK="Task: \"%s\""
 COM_SCHEDULER_TEST_RUN_TITLE="Test task (ID: %d)"
 COM_SCHEDULER_TEST_TASK="Test Task"
+COM_SCHEDULER_TOOLBAR_PURGE="Clear History"
+COM_SCHEDULER_TOOLBAR_PURGE_CONFIRM="Are you sure you want to delete all Execution History?"
 COM_SCHEDULER_TOOLBAR_UNLOCK="Unlock"
 COM_SCHEDULER_TYPE_CHOOSE="Select a Task type"
 COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND="The task routine for this task could not be found!<br>It's likely that the provider plugin was removed or disabled."
diff --git a/installation/sql/mysql/extensions.sql b/installation/sql/mysql/extensions.sql
index bffc70e49c150..7d4678ca5c4e9 100644
--- a/installation/sql/mysql/extensions.sql
+++ b/installation/sql/mysql/extensions.sql
@@ -937,6 +937,29 @@ INSERT INTO `#__scheduler_tasks` (`id`, `asset_id`, `title`, `type`, `execution_
 
 -- --------------------------------------------------------
 
+--
+-- Table structure for table `#__scheduler_logs`
+--
+
+CREATE TABLE IF NOT EXISTS `#__scheduler_logs` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `taskname` varchar(255) NOT NULL DEFAULT '',
+  `tasktype` varchar(128) NOT NULL COMMENT 'unique identifier for job defined by plugin',
+  `duration` DECIMAL(5,3) NOT NULL,
+  `jobid` int UNSIGNED NOT NULL,
+  `taskid` int UNSIGNED NOT NULL,
+  `exitcode` int NOT NULL,
+  `lastdate` datetime COMMENT 'Timestamp of last run',
+  `nextdate` datetime COMMENT 'Timestamp of next (planned) run, referred for execution on trigger',
+  PRIMARY KEY (id),
+  KEY `idx_taskname` (`taskname`),
+  KEY `idx_tasktype` (`tasktype`),
+  KEY `idx_lastdate` (`lastdate`),
+  KEY `idx_nextdate` (`nextdate`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci;
+
+-- --------------------------------------------------------
+
 --
 -- Table structure for table `#__schemaorg`
 --
diff --git a/installation/sql/postgresql/extensions.sql b/installation/sql/postgresql/extensions.sql
index 3a5188e9d261f..6cb5dd37573eb 100644
--- a/installation/sql/postgresql/extensions.sql
+++ b/installation/sql/postgresql/extensions.sql
@@ -899,6 +899,29 @@ INSERT INTO "#__scheduler_tasks" ("id", "asset_id", "title", "type", "execution_
 (3, 99, 'Update Notification', 'update.notification', CONCAT('{"rule-type":"interval-hours","interval-hours":"24","exec-day":"01","exec-time":"', TO_CHAR(CURRENT_TIMESTAMP AT TIME ZONE 'UTC', 'HH24:00'), '"}'), '{"type":"interval","exp":"PT24H"}', 1, NULL, TO_TIMESTAMP(TO_CHAR(CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + INTERVAL '24 hours', 'YYYY-MM-DD HH24:00:00'), 'YYYY-MM-DD HH24:MI:SS'), NULL, '{"individual_log":false,"log_file":"","notifications":{"success_mail":"0","failure_mail":"1","fatal_failure_mail":"1","orphan_mail":"1"},"email":"","language_override":""}', CURRENT_TIMESTAMP AT TIME ZONE 'UTC', 42);
 
 SELECT setval('#__scheduler_tasks_id_seq', 4, false);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table "#__scheduler_logs"
+--
+
+CREATE TABLE IF NOT EXISTS "#__scheduler_logs" (
+  "id" serial NOT NULL,
+  "taskname" varchar(255) DEFAULT '' NOT NULL,
+  "tasktype" varchar(128) NOT NULL,
+  "duration" NUMERIC(5,3) NOT NULL,
+  "jobid" integer NOT NULL,
+  "taskid" integer NOT NULL,
+  "exitcode" integer NOT NULL,
+  "lastdate" timestamp without time zone,
+  "nextdate" timestamp without time zone,
+  PRIMARY KEY (id)
+);
+CREATE INDEX "#__scheduler_logs_idx_taskname" ON "#__scheduler_logs" ("taskname");
+CREATE INDEX "#__scheduler_logs_idx_tasktype" ON "#__scheduler_logs" ("tasktype");
+CREATE INDEX "#__scheduler_logs_idx_lastdate" ON "#__scheduler_logs" ("lastdate");
+CREATE INDEX "#__scheduler_logs_idx_nextdate" ON "#__scheduler_logs" ("nextdate");
+
 
 -- --------------------------------------------------------
 
diff --git a/plugins/system/tasknotification/src/Extension/TaskNotification.php b/plugins/system/tasknotification/src/Extension/TaskNotification.php
index 3046974ecf23a..6be75dec3dade 100644
--- a/plugins/system/tasknotification/src/Extension/TaskNotification.php
+++ b/plugins/system/tasknotification/src/Extension/TaskNotification.php
@@ -12,6 +12,7 @@
 
 use Joomla\CMS\Event\Model;
 use Joomla\CMS\Factory;
+use Joomla\CMS\Form\Form;
 use Joomla\CMS\Log\Log;
 use Joomla\CMS\Mail\MailTemplate;
 use Joomla\CMS\Plugin\CMSPlugin;
@@ -63,11 +64,12 @@ final class TaskNotification extends CMSPlugin implements SubscriberInterface
     public static function getSubscribedEvents(): array
     {
         return [
-            'onContentPrepareForm'  => 'injectTaskNotificationFieldset',
-            'onTaskExecuteSuccess'  => 'notifySuccess',
-            'onTaskExecuteFailure'  => 'notifyFailure',
-            'onTaskRoutineNotFound' => 'notifyOrphan',
-            'onTaskRecoverFailure'  => 'notifyFatalRecovery',
+            'onContentPrepareForm'    => 'injectTaskNotificationFieldset',
+            'onTaskExecuteSuccess'    => 'notifySuccess',
+            'onTaskRoutineWillResume' => 'notifyWillResume',
+            'onTaskExecuteFailure'    => 'notifyFailure',
+            'onTaskRoutineNotFound'   => 'notifyOrphan',
+            'onTaskRecoverFailure'    => 'notifyFatalRecovery',
         ];
     }
 
@@ -124,6 +126,13 @@ public function notifyFailure(Event $event): void
         /** @var Task $task */
         $task = $event->getArgument('subject');
 
+        // @todo safety checks, multiple files [?]
+        $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? '';
+        $data    = $this->getDataFromTask($event->getArgument('subject'));
+        $model   = $this->getApplication()->bootComponent('com_scheduler')
+            ->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]);
+        $model->logTask($data);
+
         if (!(int) $task->get('params.notifications.failure_mail', 1)) {
             return;
         }
@@ -131,9 +140,6 @@ public function notifyFailure(Event $event): void
         // Load translations
         $this->loadLanguage();
 
-        // @todo safety checks, multiple files [?]
-        $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? '';
-        $data    = $this->getDataFromTask($event->getArgument('subject'));
         $this->sendMail('plg_system_tasknotification.failure_mail', $data, $outFile);
     }
 
@@ -180,19 +186,40 @@ public function notifySuccess(Event $event): void
         /** @var Task $task */
         $task = $event->getArgument('subject');
 
+        // @todo safety checks, multiple files [?]
+        $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? '';
+        $data    = $this->getDataFromTask($event->getArgument('subject'));
+        $model   = $this->getApplication()->bootComponent('com_scheduler')
+            ->getMVCFactory()->createModel('Logs', 'Administrator', ['ignore_request' => true]);
+        $model->logTask($data);
+
         if (!(int) $task->get('params.notifications.success_mail', 0)) {
             return;
         }
 
         // Load translations
         $this->loadLanguage();
-
-        // @todo safety checks, multiple files [?]
-        $outFile = $event->getArgument('subject')->snapshot['output_file'] ?? '';
-        $data    = $this->getDataFromTask($event->getArgument('subject'));
         $this->sendMail('plg_system_tasknotification.success_mail', $data, $outFile);
     }
 
+    /**
+     * Log Task execution will resume.
+     *
+     * @param   Event  $event  The onTaskRoutineWillResume event.
+     *
+     * @return void
+     *
+     * @since __DEPLOY_VERSION__
+     * @throws \Exception
+     */
+    public function notifyWillResume(Event $event): void
+    {
+        $data  = $this->getDataFromTask($event->getArgument('subject'));
+        $model = $this->getApplication()->bootComponent('com_scheduler')
+            ->getMVCFactory()->createModel('Logs', 'Administrator', ['ignore_request' => true]);
+        $model->logTask($data);
+    }
+
     /**
      * Send out email notifications on fatal recovery of task execution if task configuration allows.<br/>
      * Fatal recovery indicated that the task either crashed the parent process or its execution lasted longer
@@ -217,9 +244,6 @@ public function notifyFatalRecovery(Event $event): void
             return;
         }
 
-        // Load translations
-        $this->loadLanguage();
-
         $data = $this->getDataFromTask($event->getArgument('subject'));
         $this->sendMail('plg_system_tasknotification.fatal_recovery_mail', $data);
     }
@@ -238,9 +262,12 @@ private function getDataFromTask(Task $task): array
         return [
             'TASK_ID'        => $task->get('id'),
             'TASK_TITLE'     => $task->get('title'),
+            'TASK_TYPE'      => $task->get('type'),
             'EXIT_CODE'      => $task->getContent()['status'] ?? Status::NO_EXIT,
             'EXEC_DATE_TIME' => $lockOrExecTime,
             'TASK_OUTPUT'    => $task->getContent()['output_body'] ?? '',
+            'TASK_TIMES'     => $task->get('times_executed'),
+            'TASK_DURATION'  => $task->getContent()['duration'],
         ];
     }
 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

4 participants