summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog7
-rw-r--r--app/Action/Base.php2
-rw-r--r--app/Action/TaskCloseNoActivity.php95
-rw-r--r--app/Action/TaskEmailNoActivity.php124
-rw-r--r--app/Console/Cronjob.php33
-rw-r--r--app/Console/TaskTrigger.php52
-rw-r--r--app/Core/Event/EventManager.php1
-rw-r--r--app/Event/GenericEvent.php2
-rw-r--r--app/Event/TaskListEvent.php11
-rw-r--r--app/Model/Task.php1
-rw-r--r--app/ServiceProvider/ActionProvider.php4
-rw-r--r--app/Template/action/params.php15
-rw-r--r--doc/analytics.markdown7
-rw-r--r--doc/centos-installation.markdown2
-rw-r--r--doc/cli.markdown27
-rw-r--r--doc/cronjob.markdown32
-rw-r--r--doc/debian-installation.markdown2
-rw-r--r--doc/freebsd-installation.markdown7
-rw-r--r--doc/heroku.markdown4
-rw-r--r--doc/index.markdown1
-rw-r--r--doc/installation.markdown5
-rw-r--r--doc/ubuntu-installation.markdown2
-rw-r--r--doc/windows-apache-installation.markdown5
-rw-r--r--doc/windows-iis-installation.markdown6
-rwxr-xr-xkanboard4
25 files changed, 421 insertions, 30 deletions
diff --git a/ChangeLog b/ChangeLog
index df6a12ed..5fa09689 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -6,6 +6,13 @@ New features:
* Add project owner (Directly Responsible Individual)
* Add configurable task priority
* Add Greek translation
+* Add automatic actions to close tasks with no activity
+* Add automatic actions to send an email when there is no activity on a task
+* Regroup all daily background tasks in one command: "cronjob"
+
+Improvements:
+
+* Show progress for task links in board tooltips
Version 1.0.24
--------------
diff --git a/app/Action/Base.php b/app/Action/Base.php
index efc52f04..e8449d0c 100644
--- a/app/Action/Base.php
+++ b/app/Action/Base.php
@@ -125,7 +125,7 @@ abstract class Base extends \Kanboard\Core\Base
$params[] = $key.'='.var_export($value, true);
}
- return $this->getName().'('.implode('|', $params).'])';
+ return $this->getName().'('.implode('|', $params).')';
}
/**
diff --git a/app/Action/TaskCloseNoActivity.php b/app/Action/TaskCloseNoActivity.php
new file mode 100644
index 00000000..59f7f56a
--- /dev/null
+++ b/app/Action/TaskCloseNoActivity.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\Task;
+
+/**
+ * Close automatically a task after when inactive
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskCloseNoActivity extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Close a task when there is no activity');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(Task::EVENT_DAILY_CRONJOB);
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'duration' => t('Duration in days')
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+ }
+
+ /**
+ * Execute the action (close the task)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+
+ foreach ($data['tasks'] as $task) {
+ $duration = time() - $task['date_modification'];
+
+ if ($duration > $max) {
+ $results[] = $this->taskStatus->close($task['id']);
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+}
diff --git a/app/Action/TaskEmailNoActivity.php b/app/Action/TaskEmailNoActivity.php
new file mode 100644
index 00000000..c5d7a797
--- /dev/null
+++ b/app/Action/TaskEmailNoActivity.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\Task;
+
+/**
+ * Email a task with no activity
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskEmailNoActivity extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send email when there is no activity on a task');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_DAILY_CRONJOB,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'user_id' => t('User that will receive the email'),
+ 'subject' => t('Email subject'),
+ 'duration' => t('Duration in days'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+
+ /**
+ * Execute the action (move the task to another column)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+ $user = $this->user->getById($this->getParam('user_id'));
+
+ if (! empty($user['email'])) {
+ foreach ($data['tasks'] as $task) {
+ $duration = time() - $task['date_modification'];
+
+ if ($duration > $max) {
+ $results[] = $this->sendEmail($task['id'], $user);
+ }
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+
+ /**
+ * Send email
+ *
+ * @access private
+ * @param integer $task_id
+ * @param array $user
+ * @return boolean
+ */
+ private function sendEmail($task_id, array $user)
+ {
+ $task = $this->taskFinder->getDetails($task_id);
+
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url')))
+ );
+
+ return true;
+ }
+}
diff --git a/app/Console/Cronjob.php b/app/Console/Cronjob.php
new file mode 100644
index 00000000..2b12d93d
--- /dev/null
+++ b/app/Console/Cronjob.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Output\NullOutput;
+
+class Cronjob extends Base
+{
+ private $commands = array(
+ 'projects:daily-stats',
+ 'notification:overdue-tasks',
+ 'trigger:tasks',
+ );
+
+ protected function configure()
+ {
+ $this
+ ->setName('cronjob')
+ ->setDescription('Execute daily cronjob');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ foreach ($this->commands as $command) {
+ $job = $this->getApplication()->find($command);
+ $job->run(new ArrayInput(array('command' => $command)), new NullOutput());
+ }
+ }
+}
diff --git a/app/Console/TaskTrigger.php b/app/Console/TaskTrigger.php
new file mode 100644
index 00000000..000d215a
--- /dev/null
+++ b/app/Console/TaskTrigger.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Kanboard\Model\Task;
+use Kanboard\Event\TaskListEvent;
+
+class TaskTrigger extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('trigger:tasks')
+ ->setDescription('Trigger scheduler event for all tasks');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ foreach ($this->getProjectIds() as $project_id) {
+ $tasks = $this->taskFinder->getAll($project_id);
+ $nb_tasks = count($tasks);
+
+ if ($nb_tasks > 0) {
+ $output->writeln('Trigger task event: project_id='.$project_id.', nb_tasks='.$nb_tasks);
+ $this->sendEvent($tasks, $project_id);
+ }
+ }
+ }
+
+ private function getProjectIds()
+ {
+ $listeners = $this->dispatcher->getListeners(Task::EVENT_DAILY_CRONJOB);
+ $project_ids = array();
+
+ foreach ($listeners as $listener) {
+ $project_ids[] = $listener[0]->getProjectId();
+ }
+
+ return array_unique($project_ids);
+ }
+
+ private function sendEvent(array &$tasks, $project_id)
+ {
+ $event = new TaskListEvent(array('project_id' => $project_id));
+ $event->setTasks($tasks);
+
+ $this->dispatcher->dispatch(Task::EVENT_DAILY_CRONJOB, $event);
+ }
+}
diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php
index 8d76bfcb..162d23e8 100644
--- a/app/Core/Event/EventManager.php
+++ b/app/Core/Event/EventManager.php
@@ -52,6 +52,7 @@ class EventManager
Task::EVENT_CLOSE => t('Closing a task'),
Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
+ Task::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'),
);
$events = array_merge($events, $this->events);
diff --git a/app/Event/GenericEvent.php b/app/Event/GenericEvent.php
index 1129fd16..94a51479 100644
--- a/app/Event/GenericEvent.php
+++ b/app/Event/GenericEvent.php
@@ -7,7 +7,7 @@ use Symfony\Component\EventDispatcher\Event as BaseEvent;
class GenericEvent extends BaseEvent implements ArrayAccess
{
- private $container = array();
+ protected $container = array();
public function __construct(array $values = array())
{
diff --git a/app/Event/TaskListEvent.php b/app/Event/TaskListEvent.php
new file mode 100644
index 00000000..9be1a7d9
--- /dev/null
+++ b/app/Event/TaskListEvent.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Kanboard\Event;
+
+class TaskListEvent extends GenericEvent
+{
+ public function setTasks(array &$tasks)
+ {
+ $this->container['tasks'] =& $tasks;
+ }
+}
diff --git a/app/Model/Task.php b/app/Model/Task.php
index 7aa9e312..94b23ec2 100644
--- a/app/Model/Task.php
+++ b/app/Model/Task.php
@@ -42,6 +42,7 @@ class Task extends Base
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
const EVENT_OVERDUE = 'task.overdue';
const EVENT_USER_MENTION = 'task.user.mention';
+ const EVENT_DAILY_CRONJOB = 'task.cronjob.daily';
/**
* Recurrence: status
diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php
index 0aba29f1..3692f190 100644
--- a/app/ServiceProvider/ActionProvider.php
+++ b/app/ServiceProvider/ActionProvider.php
@@ -23,12 +23,14 @@ use Kanboard\Action\TaskCloseColumn;
use Kanboard\Action\TaskCreation;
use Kanboard\Action\TaskDuplicateAnotherProject;
use Kanboard\Action\TaskEmail;
+use Kanboard\Action\TaskEmailNoActivity;
use Kanboard\Action\TaskMoveAnotherProject;
use Kanboard\Action\TaskMoveColumnAssigned;
use Kanboard\Action\TaskMoveColumnCategoryChange;
use Kanboard\Action\TaskMoveColumnUnAssigned;
use Kanboard\Action\TaskOpen;
use Kanboard\Action\TaskUpdateStartDate;
+use Kanboard\Action\TaskCloseNoActivity;
/**
* Action Provider
@@ -63,9 +65,11 @@ class ActionProvider implements ServiceProviderInterface
$container['actionManager']->register(new TaskAssignUser($container));
$container['actionManager']->register(new TaskClose($container));
$container['actionManager']->register(new TaskCloseColumn($container));
+ $container['actionManager']->register(new TaskCloseNoActivity($container));
$container['actionManager']->register(new TaskCreation($container));
$container['actionManager']->register(new TaskDuplicateAnotherProject($container));
$container['actionManager']->register(new TaskEmail($container));
+ $container['actionManager']->register(new TaskEmailNoActivity($container));
$container['actionManager']->register(new TaskMoveAnotherProject($container));
$container['actionManager']->register(new TaskMoveColumnAssigned($container));
$container['actionManager']->register(new TaskMoveColumnCategoryChange($container));
diff --git a/app/Template/action/params.php b/app/Template/action/params.php
index dcfaa9cc..a2350dea 100644
--- a/app/Template/action/params.php
+++ b/app/Template/action/params.php
@@ -15,22 +15,25 @@
<?php if ($this->text->contains($param_name, 'column_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'user_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'project_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'color_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'category_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'link_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
- <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?><br/>
+ <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'duration')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->number('params['.$param_name.']', $values) ?>
<?php else: ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->text('params['.$param_name.']', $values) ?>
diff --git a/doc/analytics.markdown b/doc/analytics.markdown
index 13657b56..d72fc383 100644
--- a/doc/analytics.markdown
+++ b/doc/analytics.markdown
@@ -62,9 +62,4 @@ This chart show the average lead and cycle time for the last 1000 tasks over tim
Those metrics are calculated and recorded every day for the whole project.
-Don't forget to run the daily job for statistics calculation
--------------------------------------------------------
-
-To generate accurate analytic data, you should run the daily cronjob **project daily statistics**.
-
-[Read the documentation of Kanboard CLI](cli.markdown)
+Note: Don't forget to run the [daily cronjob](cronjob.markdown) to have accurate statistics.
diff --git a/doc/centos-installation.markdown b/doc/centos-installation.markdown
index d0fd6a00..576119b4 100644
--- a/doc/centos-installation.markdown
+++ b/doc/centos-installation.markdown
@@ -1,6 +1,8 @@
Centos Installation
===================
+Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
+
Centos 7
--------
diff --git a/doc/cli.markdown b/doc/cli.markdown
index bcb478dd..9334d84b 100644
--- a/doc/cli.markdown
+++ b/doc/cli.markdown
@@ -4,7 +4,7 @@ Command Line Interface
Kanboard provides a simple command line interface that can be used from any Unix terminal.
This tool can be used only on the local machine.
-This feature is useful to run commands outside the web server process by example running a huge report.
+This feature is useful to run commands outside of the web server processes.
Usage
-----
@@ -28,6 +28,7 @@ Options:
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
+ cronjob Execute daily cronjob
help Displays help for a command
list Lists commands
export
@@ -42,6 +43,8 @@ Available commands:
notification:overdue-tasks Send notifications for overdue tasks
projects
projects:daily-stats Calculate daily statistics for all projects
+ trigger
+ trigger:tasks Trigger scheduler event for all tasks
```
Available commands
@@ -116,7 +119,7 @@ Emails will be sent to all users with notifications enabled.
You can also display the overdue tasks with the flag `--show`:
```bash
-$ ./kanboard notification:overdue-tasks --show
+./kanboard notification:overdue-tasks --show
+-----+---------+------------+------------+--------------+----------+
| Id | Title | Due date | Project Id | Project name | Assignee |
+-----+---------+------------+------------+--------------+----------+
@@ -125,20 +128,22 @@ $ ./kanboard notification:overdue-tasks --show
+-----+---------+------------+------------+--------------+----------+
```
-Cronjob example:
-
-```bash
-# Everyday at 8am we check for due tasks
-0 8 * * * cd /path/to/kanboard && ./kanboard notification:overdue-tasks >/dev/null 2>&1
-```
-
### Run daily project stats calculation
-You can add a background task to calculate the project statistics every day:
+This command calculate the statistics of each project:
```bash
-$ ./kanboard projects:daily-stats
+./kanboard projects:daily-stats
Run calculation for Project #0
Run calculation for Project #1
Run calculation for Project #10
```
+
+### Trigger for tasks
+
+This command send a "daily cronjob event" to all open tasks of each project.
+
+```bash
+./kanboard trigger:tasks
+Trigger task event: project_id=2, nb_tasks=1
+```
diff --git a/doc/cronjob.markdown b/doc/cronjob.markdown
new file mode 100644
index 00000000..32f12888
--- /dev/null
+++ b/doc/cronjob.markdown
@@ -0,0 +1,32 @@
+Background Job Scheduling
+=========================
+
+To work properly, Kanboard requires that a background job run on a daily basis.
+Usually on Unix platforms, this process is done by `cron`.
+
+This background job is necessary for these features:
+
+- Reports and analytics (calculate daily stats of each projects)
+- Send overdue task notifications
+- Execute automatic actions connected to the event "Daily background job for tasks"
+
+Configuration on Unix and Linux platforms
+-----------------------------------------
+
+There are multiple ways to define a cronjob on Unix/Linux operating systems, this example is for Ubuntu 14.04.
+The procedure is similar to other systems.
+
+Edit the crontab of your web server user:
+
+```bash
+sudo crontab -u www-data -e
+```
+
+Example to execute the daily cronjob at 8am:
+
+```bash
+0 8 * * * cd /path/to/kanboard && ./kanboard cronjob >/dev/null 2>&1
+```
+
+Note: the cronjob process must have write access to the database in case you are using Sqlite.
+Usually, running the cronjob under the web server user is enough.
diff --git a/doc/debian-installation.markdown b/doc/debian-installation.markdown
index 147fe452..ec956049 100644
--- a/doc/debian-installation.markdown
+++ b/doc/debian-installation.markdown
@@ -1,6 +1,8 @@
How to install Kanboard on Debian?
==================================
+Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
+
Debian 8 (Jessie)
-----------------
diff --git a/doc/freebsd-installation.markdown b/doc/freebsd-installation.markdown
index 84b35ad8..7b36dff1 100644
--- a/doc/freebsd-installation.markdown
+++ b/doc/freebsd-installation.markdown
@@ -55,7 +55,7 @@ Generally 3 elements have to be installed:
Fetch and extract ports...
```bash
-$ portsnap fetch
+$ portsnap fetch
$ portsnap extract
```
@@ -122,6 +122,7 @@ there is no need to install it manually.
Please note
-----------
-Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment,
+- Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment,
fork and suggest updates!
- \ No newline at end of file
+- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
+
diff --git a/doc/heroku.markdown b/doc/heroku.markdown
index 56d79bc9..f145f70e 100644
--- a/doc/heroku.markdown
+++ b/doc/heroku.markdown
@@ -35,5 +35,5 @@ heroku open
Limitations
-----------
-The storage of Heroku is ephemeral, that means uploaded files through Kanboard are not persistent after a reboot.
-We may want to install a plugin to store your files in a cloud storage provider like [Amazon S3](https://github.com/kanboard/plugin-s3).
+- The storage of Heroku is ephemeral, that means uploaded files through Kanboard are not persistent after a reboot. You may want to install a plugin to store your files in a cloud storage provider like [Amazon S3](https://github.com/kanboard/plugin-s3).
+- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
diff --git a/doc/index.markdown b/doc/index.markdown
index 1e95fe06..7603117b 100644
--- a/doc/index.markdown
+++ b/doc/index.markdown
@@ -103,6 +103,7 @@ Technical details
### Configuration
- [Config file](config.markdown)
+- [Background tasks](cronjob.markdown)
- [Email configuration](email-configuration.markdown)
- [URL rewriting](nice-urls.markdown)
diff --git a/doc/installation.markdown b/doc/installation.markdown
index b208be8a..dd4283f8 100644
--- a/doc/installation.markdown
+++ b/doc/installation.markdown
@@ -39,3 +39,8 @@ Security
- Don't forget to change the default user/password
- Don't allow everybody to access to the directory `data` from the URL. There is already a `.htaccess` for Apache but nothing for Nginx.
+
+Notes
+-----
+
+- Some features of Kanboard require that you run [a daily background job](cronjob.markdown)
diff --git a/doc/ubuntu-installation.markdown b/doc/ubuntu-installation.markdown
index cec3ebba..ab4dfe7c 100644
--- a/doc/ubuntu-installation.markdown
+++ b/doc/ubuntu-installation.markdown
@@ -26,3 +26,5 @@ sudo unzip kanboard-latest.zip
sudo chown -R www-data:www-data kanboard/data
sudo rm kanboard-latest.zip
```
+
+Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
diff --git a/doc/windows-apache-installation.markdown b/doc/windows-apache-installation.markdown
index 2c8f74e1..27b6812e 100644
--- a/doc/windows-apache-installation.markdown
+++ b/doc/windows-apache-installation.markdown
@@ -123,3 +123,8 @@ Tested configuration
--------------------
- Windows 2008 R2 / Apache 2.4.12 / PHP 5.6.8
+
+Notes
+-----
+
+- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
diff --git a/doc/windows-iis-installation.markdown b/doc/windows-iis-installation.markdown
index 6206db21..bd4607de 100644
--- a/doc/windows-iis-installation.markdown
+++ b/doc/windows-iis-installation.markdown
@@ -65,3 +65,9 @@ Tested configurations
- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16
- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29
+
+Notes
+-----
+
+- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
+
diff --git a/kanboard b/kanboard
index 73dab4e9..5046181d 100755
--- a/kanboard
+++ b/kanboard
@@ -13,6 +13,8 @@ use Kanboard\Console\ProjectDailyColumnStatsExport;
use Kanboard\Console\TransitionExport;
use Kanboard\Console\LocaleSync;
use Kanboard\Console\LocaleComparator;
+use Kanboard\Console\TaskTrigger;
+use Kanboard\Console\Cronjob;
$container['dispatcher']->dispatch('app.bootstrap', new Event);
@@ -25,4 +27,6 @@ $application->add(new ProjectDailyColumnStatsExport($container));
$application->add(new TransitionExport($container));
$application->add(new LocaleSync($container));
$application->add(new LocaleComparator($container));
+$application->add(new TaskTrigger($container));
+$application->add(new Cronjob($container));
$application->run();