diff --git a/core/templates/pages/command-routing.html b/core/templates/pages/command-routing.html index f06d848..d397d1f 100644 --- a/core/templates/pages/command-routing.html +++ b/core/templates/pages/command-routing.html @@ -150,9 +150,11 @@ {% if variant.warn_verbatim_plan %} -
- Warning: {{ variant.variant_label }} is in verbatim mode with plan fanout enabled. - Recipients will get raw transcript-style output. +
+
+ Warning: {{ variant.variant_label }} is in verbatim mode with plan fanout enabled. + Recipients will get raw transcript-style output. +
@@ -188,15 +190,17 @@

Effective Destinations

{% if profile.enabled_egress_bindings %} - -

{{ profile.enabled_egress_bindings|length }} enabled egress destination{{ profile.enabled_egress_bindings|length|pluralize }}.

+ +
+ {{ profile.enabled_egress_bindings|length }} enabled egress destination{{ profile.enabled_egress_bindings|length|pluralize }}. +
{% else %}
No enabled egress destinations. Plan fanout will show sent:0.
{% endif %} @@ -441,51 +445,5 @@ .command-order-capsule-form + .command-order-capsule-form .command-order-btn { border-top: 1px solid #dbdbdb; } - .command-variant-warning { - border: 1px solid rgba(171, 109, 17, 0.45); - background: linear-gradient(180deg, rgba(255, 246, 226, 0.98), rgba(255, 238, 204, 0.95)); - color: #6e450e; - border-radius: 8px; - padding: 0.48rem 0.62rem; - font-size: 0.78rem; - line-height: 1.35; - } - .command-variant-warning strong { - color: #3f2a09; - } - .command-variant-warning code { - color: #5b3a0c; - background: rgba(255, 255, 255, 0.55); - } - .command-destination-list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.28rem; - } - .command-destination-item { - display: inline-flex; - align-items: center; - gap: 0.42rem; - background: rgba(244, 248, 255, 0.85); - border: 1px solid rgba(58, 103, 165, 0.2); - border-radius: 7px; - padding: 0.26rem 0.38rem; - width: fit-content; - max-width: 100%; - } - .command-destination-summary { - margin-top: 0.44rem; - display: inline-flex; - align-items: center; - border-radius: 999px; - background: rgba(239, 247, 255, 0.95); - border: 1px solid rgba(58, 103, 165, 0.25); - padding: 0.16rem 0.52rem; - font-size: 0.73rem; - color: #284d7c; - } {% endblock %} diff --git a/core/templates/pages/tasks-detail.html b/core/templates/pages/tasks-detail.html index 9b5297f..f913713 100644 --- a/core/templates/pages/tasks-detail.html +++ b/core/templates/pages/tasks-detail.html @@ -25,7 +25,19 @@
{{ row.actor_identifier }}
{% endif %} - {{ row.payload }} + + {% if row.payload_view.summary_items %} +
+ {% for item in row.payload_view.summary_items %} + {{ item.0 }}: {{ item.1 }} + {% endfor %} +
+ {% endif %} +
+ View payload JSON +
{{ row.payload_view.pretty_text }}
+
+ {% empty %} No events. @@ -47,4 +59,23 @@
+ {% endblock %} diff --git a/core/tests/test_tasks_pages_management.py b/core/tests/test_tasks_pages_management.py index 06703e0..13dc73d 100644 --- a/core/tests/test_tasks_pages_management.py +++ b/core/tests/test_tasks_pages_management.py @@ -7,6 +7,7 @@ from core.models import ( ChatSession, ChatTaskSource, DerivedTask, + DerivedTaskEvent, Message, Person, PersonIdentifier, @@ -154,3 +155,38 @@ class TasksPagesManagementTests(TestCase): response = self.client.get(reverse("tasks_hub")) self.assertEqual(200, response.status_code) self.assertContains(response, "Scope Person") + + def test_task_detail_renders_payload_summary_and_json(self): + project = TaskProject.objects.create(user=self.user, name="Payload Test") + session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal) + origin = Message.objects.create( + user=self.user, + session=session, + ts=1_700_000_000_000, + text="origin", + sender_uuid="+15551230000", + custom_author="OTHER", + source_service="signal", + source_chat_id="+15551230000", + ) + task = DerivedTask.objects.create( + user=self.user, + project=project, + title="Payload detail", + source_service="signal", + source_channel="+15551230000", + origin_message=origin, + reference_code="42", + status_snapshot="open", + ) + DerivedTaskEvent.objects.create( + task=task, + event_type="reaction_captured", + payload={"source": "signal", "emoji": "❤️", "reason": "heart_reaction"}, + ) + + response = self.client.get(reverse("tasks_task", kwargs={"task_id": str(task.id)})) + self.assertEqual(200, response.status_code) + self.assertContains(response, "View payload JSON") + self.assertContains(response, "source: signal", html=True) + self.assertContains(response, ""emoji": "❤️"") diff --git a/core/views/tasks.py b/core/views/tasks.py index 0a337d0..989a654 100644 --- a/core/views/tasks.py +++ b/core/views/tasks.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from urllib.parse import urlencode from asgiref.sync import async_to_sync @@ -174,6 +175,60 @@ def _service_label(service: str) -> str: return labels.get(key, key.title() if key else "Unknown") +def _format_task_event_payload(raw_payload): + payload = raw_payload + if payload is None: + payload = {} + if isinstance(payload, str): + text = payload.strip() + if not text: + return { + "summary_items": [], + "pretty_text": "{}", + "is_mapping": False, + } + try: + parsed = json.loads(text) + payload = parsed + except Exception: + return { + "summary_items": [("text", text[:140])], + "pretty_text": text, + "is_mapping": False, + } + + if isinstance(payload, dict): + summary = [] + preferred = ("source", "reason", "reaction", "emoji", "presence", "last_seen_ts") + for key in preferred: + if key in payload: + summary.append((key, str(payload.get(key)))) + if not summary: + for key in list(payload.keys())[:4]: + summary.append((str(key), str(payload.get(key)))) + pretty = json.dumps(payload, indent=2, ensure_ascii=False, sort_keys=True) + return { + "summary_items": summary, + "pretty_text": pretty, + "is_mapping": True, + } + + if isinstance(payload, (list, tuple)): + pretty = json.dumps(list(payload), indent=2, ensure_ascii=False) + return { + "summary_items": [("items", str(len(payload)))], + "pretty_text": pretty, + "is_mapping": False, + } + + text = str(payload) + return { + "summary_items": [("value", text[:140])], + "pretty_text": text, + "is_mapping": False, + } + + def _creator_label_for_message(user, service: str, message) -> str: msg = message if msg is None: @@ -800,6 +855,7 @@ class TaskDetail(LoginRequiredMixin, View): raw_actor = str(getattr(row, "actor_identifier", "") or "").strip() if raw_actor: row.actor_display = raw_actor + row.payload_view = _format_task_event_payload(getattr(row, "payload", None)) task.creator_label = _creator_label_for_message( request.user, str(getattr(task, "source_service", "") or "").strip().lower(),