prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  System Tasks: Show start timestamp and calculate Duration
  **Is your feature request related to a problem? Please describe.**
  For debugging purposes, I need the info when a task started, and when it finished. 
  
  **Describe the solution you'd like**
  I have seen that the TaskInfo object actually holds that information, but it is not returned by the API, and not shown in the "SystemTasks" table of the web UI. 
  It would also make sense to calculate the duration for easier debugging.
  
  **Describe alternatives you've considered**
  I could look this up in the database, but this would be questionable UX, since there is already a view in the web app which should show this information.
  
  **Additional context**
  (none)
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `authentik/admin/api/tasks.py`
  Content:
  ```
  1 """Tasks API"""
  2 from importlib import import_module
  3 
  4 from django.contrib import messages
  5 from django.http.response import Http404
  6 from django.utils.translation import gettext_lazy as _
  7 from drf_spectacular.types import OpenApiTypes
  8 from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
  9 from rest_framework.decorators import action
  10 from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
  11 from rest_framework.permissions import IsAdminUser
  12 from rest_framework.request import Request
  13 from rest_framework.response import Response
  14 from rest_framework.viewsets import ViewSet
  15 from structlog.stdlib import get_logger
  16 
  17 from authentik.core.api.utils import PassiveSerializer
  18 from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
  19 
  20 LOGGER = get_logger()
  21 
  22 
  23 class TaskSerializer(PassiveSerializer):
  24     """Serialize TaskInfo and TaskResult"""
  25 
  26     task_name = CharField()
  27     task_description = CharField()
  28     task_finish_timestamp = DateTimeField(source="finish_time")
  29 
  30     status = ChoiceField(
  31         source="result.status.name",
  32         choices=[(x.name, x.name) for x in TaskResultStatus],
  33     )
  34     messages = ListField(source="result.messages")
  35 
  36     def to_representation(self, instance):
  37         """When a new version of authentik adds fields to TaskInfo,
  38         the API will fail with an AttributeError, as the classes
  39         are pickled in cache. In that case, just delete the info"""
  40         try:
  41             return super().to_representation(instance)
  42         except AttributeError:  # pragma: no cover
  43             if isinstance(self.instance, list):
  44                 for inst in self.instance:
  45                     inst.delete()
  46             else:
  47                 self.instance.delete()
  48             return {}
  49 
  50 
  51 class TaskViewSet(ViewSet):
  52     """Read-only view set that returns all background tasks"""
  53 
  54     permission_classes = [IsAdminUser]
  55     serializer_class = TaskSerializer
  56 
  57     @extend_schema(
  58         responses={
  59             200: TaskSerializer(many=False),
  60             404: OpenApiResponse(description="Task not found"),
  61         },
  62         parameters=[
  63             OpenApiParameter(
  64                 "id",
  65                 type=OpenApiTypes.STR,
  66                 location=OpenApiParameter.PATH,
  67                 required=True,
  68             ),
  69         ],
  70     )
  71     # pylint: disable=invalid-name
  72     def retrieve(self, request: Request, pk=None) -> Response:
  73         """Get a single system task"""
  74         task = TaskInfo.by_name(pk)
  75         if not task:
  76             raise Http404
  77         return Response(TaskSerializer(task, many=False).data)
  78 
  79     @extend_schema(responses={200: TaskSerializer(many=True)})
  80     def list(self, request: Request) -> Response:
  81         """List system tasks"""
  82         tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)
  83         return Response(TaskSerializer(tasks, many=True).data)
  84 
  85     @extend_schema(
  86         request=OpenApiTypes.NONE,
  87         responses={
  88             204: OpenApiResponse(description="Task retried successfully"),
  89             404: OpenApiResponse(description="Task not found"),
  90             500: OpenApiResponse(description="Failed to retry task"),
  91         },
  92         parameters=[
  93             OpenApiParameter(
  94                 "id",
  95                 type=OpenApiTypes.STR,
  96                 location=OpenApiParameter.PATH,
  97                 required=True,
  98             ),
  99         ],
  100     )
  101     @action(detail=True, methods=["post"])
  102     # pylint: disable=invalid-name
  103     def retry(self, request: Request, pk=None) -> Response:
  104         """Retry task"""
  105         task = TaskInfo.by_name(pk)
  106         if not task:
  107             raise Http404
  108         try:
  109             task_module = import_module(task.task_call_module)
  110             task_func = getattr(task_module, task.task_call_func)
  111             LOGGER.debug("Running task", task=task_func)
  112             task_func.delay(*task.task_call_args, **task.task_call_kwargs)
  113             messages.success(
  114                 self.request,
  115                 _("Successfully re-scheduled Task %(name)s!" % {"name": task.task_name}),
  116             )
  117             return Response(status=204)
  118         except (ImportError, AttributeError):  # pragma: no cover
  119             LOGGER.warning("Failed to run task, remove state", task=task)
  120             # if we get an import error, the module path has probably changed
  121             task.delete()
  122             return Response(status=500)
  123
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py
  --- a/authentik/admin/api/tasks.py
  +++ b/authentik/admin/api/tasks.py
  @@ -7,7 +7,13 @@
   from drf_spectacular.types import OpenApiTypes
   from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
   from rest_framework.decorators import action
  -from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
  +from rest_framework.fields import (
  +    CharField,
  +    ChoiceField,
  +    DateTimeField,
  +    ListField,
  +    SerializerMethodField,
  +)
   from rest_framework.permissions import IsAdminUser
   from rest_framework.request import Request
   from rest_framework.response import Response
  @@ -26,6 +32,7 @@
       task_name = CharField()
       task_description = CharField()
       task_finish_timestamp = DateTimeField(source="finish_time")
  +    task_duration = SerializerMethodField()
   
       status = ChoiceField(
           source="result.status.name",
  @@ -33,7 +40,11 @@
       )
       messages = ListField(source="result.messages")
   
  -    def to_representation(self, instance):
  +    def get_task_duration(self, instance: TaskInfo) -> int:
  +        """Get the duration a task took to run"""
  +        return max(instance.finish_timestamp - instance.start_timestamp, 0)
  +
  +    def to_representation(self, instance: TaskInfo):
           """When a new version of authentik adds fields to TaskInfo,
           the API will fail with an AttributeError, as the classes
           are pickled in cache. In that case, just delete the info"""
  
verification_info: {"golden_diff": "diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py\n--- a/authentik/admin/api/tasks.py\n+++ b/authentik/admin/api/tasks.py\n@@ -7,7 +7,13 @@\n from drf_spectacular.types import OpenApiTypes\n from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema\n from rest_framework.decorators import action\n-from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField\n+from rest_framework.fields import (\n+    CharField,\n+    ChoiceField,\n+    DateTimeField,\n+    ListField,\n+    SerializerMethodField,\n+)\n from rest_framework.permissions import IsAdminUser\n from rest_framework.request import Request\n from rest_framework.response import Response\n@@ -26,6 +32,7 @@\n     task_name = CharField()\n     task_description = CharField()\n     task_finish_timestamp = DateTimeField(source=\"finish_time\")\n+    task_duration = SerializerMethodField()\n \n     status = ChoiceField(\n         source=\"result.status.name\",\n@@ -33,7 +40,11 @@\n     )\n     messages = ListField(source=\"result.messages\")\n \n-    def to_representation(self, instance):\n+    def get_task_duration(self, instance: TaskInfo) -> int:\n+        \"\"\"Get the duration a task took to run\"\"\"\n+        return max(instance.finish_timestamp - instance.start_timestamp, 0)\n+\n+    def to_representation(self, instance: TaskInfo):\n         \"\"\"When a new version of authentik adds fields to TaskInfo,\n         the API will fail with an AttributeError, as the classes\n         are pickled in cache. In that case, just delete the info\"\"\"\n", "issue": "System Tasks: Show start timestamp and calculate Duration\n**Is your feature request related to a problem? Please describe.**\r\nFor debugging purposes, I need the info when a task started, and when it finished. \r\n\r\n**Describe the solution you'd like**\r\nI have seen that the TaskInfo object actually holds that information, but it is not returned by the API, and not shown in the \"SystemTasks\" table of the web UI. \r\nIt would also make sense to calculate the duration for easier debugging.\r\n\r\n**Describe alternatives you've considered**\r\nI could look this up in the database, but this would be questionable UX, since there is already a view in the web app which should show this information.\r\n\r\n**Additional context**\r\n(none)\r\n\n", "before_files": [{"content": "\"\"\"Tasks API\"\"\"\nfrom importlib import import_module\n\nfrom django.contrib import messages\nfrom django.http.response import Http404\nfrom django.utils.translation import gettext_lazy as _\nfrom drf_spectacular.types import OpenApiTypes\nfrom drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema\nfrom rest_framework.decorators import action\nfrom rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField\nfrom rest_framework.permissions import IsAdminUser\nfrom rest_framework.request import Request\nfrom rest_framework.response import Response\nfrom rest_framework.viewsets import ViewSet\nfrom structlog.stdlib import get_logger\n\nfrom authentik.core.api.utils import PassiveSerializer\nfrom authentik.events.monitored_tasks import TaskInfo, TaskResultStatus\n\nLOGGER = get_logger()\n\n\nclass TaskSerializer(PassiveSerializer):\n    \"\"\"Serialize TaskInfo and TaskResult\"\"\"\n\n    task_name = CharField()\n    task_description = CharField()\n    task_finish_timestamp = DateTimeField(source=\"finish_time\")\n\n    status = ChoiceField(\n        source=\"result.status.name\",\n        choices=[(x.name, x.name) for x in TaskResultStatus],\n    )\n    messages = ListField(source=\"result.messages\")\n\n    def to_representation(self, instance):\n        \"\"\"When a new version of authentik adds fields to TaskInfo,\n        the API will fail with an AttributeError, as the classes\n        are pickled in cache. In that case, just delete the info\"\"\"\n        try:\n            return super().to_representation(instance)\n        except AttributeError:  # pragma: no cover\n            if isinstance(self.instance, list):\n                for inst in self.instance:\n                    inst.delete()\n            else:\n                self.instance.delete()\n            return {}\n\n\nclass TaskViewSet(ViewSet):\n    \"\"\"Read-only view set that returns all background tasks\"\"\"\n\n    permission_classes = [IsAdminUser]\n    serializer_class = TaskSerializer\n\n    @extend_schema(\n        responses={\n            200: TaskSerializer(many=False),\n            404: OpenApiResponse(description=\"Task not found\"),\n        },\n        parameters=[\n            OpenApiParameter(\n                \"id\",\n                type=OpenApiTypes.STR,\n                location=OpenApiParameter.PATH,\n                required=True,\n            ),\n        ],\n    )\n    # pylint: disable=invalid-name\n    def retrieve(self, request: Request, pk=None) -> Response:\n        \"\"\"Get a single system task\"\"\"\n        task = TaskInfo.by_name(pk)\n        if not task:\n            raise Http404\n        return Response(TaskSerializer(task, many=False).data)\n\n    @extend_schema(responses={200: TaskSerializer(many=True)})\n    def list(self, request: Request) -> Response:\n        \"\"\"List system tasks\"\"\"\n        tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)\n        return Response(TaskSerializer(tasks, many=True).data)\n\n    @extend_schema(\n        request=OpenApiTypes.NONE,\n        responses={\n            204: OpenApiResponse(description=\"Task retried successfully\"),\n            404: OpenApiResponse(description=\"Task not found\"),\n            500: OpenApiResponse(description=\"Failed to retry task\"),\n        },\n        parameters=[\n            OpenApiParameter(\n                \"id\",\n                type=OpenApiTypes.STR,\n                location=OpenApiParameter.PATH,\n                required=True,\n            ),\n        ],\n    )\n    @action(detail=True, methods=[\"post\"])\n    # pylint: disable=invalid-name\n    def retry(self, request: Request, pk=None) -> Response:\n        \"\"\"Retry task\"\"\"\n        task = TaskInfo.by_name(pk)\n        if not task:\n            raise Http404\n        try:\n            task_module = import_module(task.task_call_module)\n            task_func = getattr(task_module, task.task_call_func)\n            LOGGER.debug(\"Running task\", task=task_func)\n            task_func.delay(*task.task_call_args, **task.task_call_kwargs)\n            messages.success(\n                self.request,\n                _(\"Successfully re-scheduled Task %(name)s!\" % {\"name\": task.task_name}),\n            )\n            return Response(status=204)\n        except (ImportError, AttributeError):  # pragma: no cover\n            LOGGER.warning(\"Failed to run task, remove state\", task=task)\n            # if we get an import error, the module path has probably changed\n            task.delete()\n            return Response(status=500)\n", "path": "authentik/admin/api/tasks.py"}], "after_files": [{"content": "\"\"\"Tasks API\"\"\"\nfrom importlib import import_module\n\nfrom django.contrib import messages\nfrom django.http.response import Http404\nfrom django.utils.translation import gettext_lazy as _\nfrom drf_spectacular.types import OpenApiTypes\nfrom drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema\nfrom rest_framework.decorators import action\nfrom rest_framework.fields import (\n    CharField,\n    ChoiceField,\n    DateTimeField,\n    ListField,\n    SerializerMethodField,\n)\nfrom rest_framework.permissions import IsAdminUser\nfrom rest_framework.request import Request\nfrom rest_framework.response import Response\nfrom rest_framework.viewsets import ViewSet\nfrom structlog.stdlib import get_logger\n\nfrom authentik.core.api.utils import PassiveSerializer\nfrom authentik.events.monitored_tasks import TaskInfo, TaskResultStatus\n\nLOGGER = get_logger()\n\n\nclass TaskSerializer(PassiveSerializer):\n    \"\"\"Serialize TaskInfo and TaskResult\"\"\"\n\n    task_name = CharField()\n    task_description = CharField()\n    task_finish_timestamp = DateTimeField(source=\"finish_time\")\n    task_duration = SerializerMethodField()\n\n    status = ChoiceField(\n        source=\"result.status.name\",\n        choices=[(x.name, x.name) for x in TaskResultStatus],\n    )\n    messages = ListField(source=\"result.messages\")\n\n    def get_task_duration(self, instance: TaskInfo) -> int:\n        \"\"\"Get the duration a task took to run\"\"\"\n        return max(instance.finish_timestamp - instance.start_timestamp, 0)\n\n    def to_representation(self, instance: TaskInfo):\n        \"\"\"When a new version of authentik adds fields to TaskInfo,\n        the API will fail with an AttributeError, as the classes\n        are pickled in cache. In that case, just delete the info\"\"\"\n        try:\n            return super().to_representation(instance)\n        except AttributeError:  # pragma: no cover\n            if isinstance(self.instance, list):\n                for inst in self.instance:\n                    inst.delete()\n            else:\n                self.instance.delete()\n            return {}\n\n\nclass TaskViewSet(ViewSet):\n    \"\"\"Read-only view set that returns all background tasks\"\"\"\n\n    permission_classes = [IsAdminUser]\n    serializer_class = TaskSerializer\n\n    @extend_schema(\n        responses={\n            200: TaskSerializer(many=False),\n            404: OpenApiResponse(description=\"Task not found\"),\n        },\n        parameters=[\n            OpenApiParameter(\n                \"id\",\n                type=OpenApiTypes.STR,\n                location=OpenApiParameter.PATH,\n                required=True,\n            ),\n        ],\n    )\n    # pylint: disable=invalid-name\n    def retrieve(self, request: Request, pk=None) -> Response:\n        \"\"\"Get a single system task\"\"\"\n        task = TaskInfo.by_name(pk)\n        if not task:\n            raise Http404\n        return Response(TaskSerializer(task, many=False).data)\n\n    @extend_schema(responses={200: TaskSerializer(many=True)})\n    def list(self, request: Request) -> Response:\n        \"\"\"List system tasks\"\"\"\n        tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)\n        return Response(TaskSerializer(tasks, many=True).data)\n\n    @extend_schema(\n        request=OpenApiTypes.NONE,\n        responses={\n            204: OpenApiResponse(description=\"Task retried successfully\"),\n            404: OpenApiResponse(description=\"Task not found\"),\n            500: OpenApiResponse(description=\"Failed to retry task\"),\n        },\n        parameters=[\n            OpenApiParameter(\n                \"id\",\n                type=OpenApiTypes.STR,\n                location=OpenApiParameter.PATH,\n                required=True,\n            ),\n        ],\n    )\n    @action(detail=True, methods=[\"post\"])\n    # pylint: disable=invalid-name\n    def retry(self, request: Request, pk=None) -> Response:\n        \"\"\"Retry task\"\"\"\n        task = TaskInfo.by_name(pk)\n        if not task:\n            raise Http404\n        try:\n            task_module = import_module(task.task_call_module)\n            task_func = getattr(task_module, task.task_call_func)\n            LOGGER.debug(\"Running task\", task=task_func)\n            task_func.delay(*task.task_call_args, **task.task_call_kwargs)\n            messages.success(\n                self.request,\n                _(\"Successfully re-scheduled Task %(name)s!\" % {\"name\": task.task_name}),\n            )\n            return Response(status=204)\n        except (ImportError, AttributeError):  # pragma: no cover\n            LOGGER.warning(\"Failed to run task, remove state\", task=task)\n            # if we get an import error, the module path has probably changed\n            task.delete()\n            return Response(status=500)\n", "path": "authentik/admin/api/tasks.py"}]}