feat: add daily report list and detail views
- New daily_report_list_view at /admin/openclaw/session/daily-reports/ - Shows (agent, date) combos with message/session counts - Filter by agent name and date range - Click date to enter detail page - New daily_report_detail_view at /admin/openclaw/session/daily-reports/<agent>/<date>/ - Shows all messages for that agent on that date - Full message content, tool calls, arguments, results - Session metadata header per session - Added django.contrib.humanize for intcomma template filter
This commit is contained in:
@@ -11,6 +11,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.humanize",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"openclaw",
|
"openclaw",
|
||||||
]
|
]
|
||||||
|
|||||||
79
src/openclaw/admin.py
Normal file → Executable file
79
src/openclaw/admin.py
Normal file → Executable file
@@ -1,12 +1,10 @@
|
|||||||
from datetime import date
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Prefetch
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
|
||||||
from openclaw.export import export_daily_markdown
|
from openclaw.export import export_daily_markdown
|
||||||
from openclaw.models import Session, Message, ToolCall
|
from openclaw.models import Session, Message, ToolCall
|
||||||
|
from openclaw.admin_new_views import daily_report_list_view, daily_report_detail_view
|
||||||
|
|
||||||
|
|
||||||
class MessageInline(admin.TabularInline):
|
class MessageInline(admin.TabularInline):
|
||||||
@@ -35,73 +33,6 @@ class ToolCallInline(admin.TabularInline):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def daily_conversation_view(request):
|
|
||||||
"""Admin standalone view for date-range conversation browsing."""
|
|
||||||
start_str = request.GET.get("start")
|
|
||||||
end_str = request.GET.get("end")
|
|
||||||
agent_filter = request.GET.get("agent", "")
|
|
||||||
|
|
||||||
start_date = start_str if start_str else date.today().isoformat()
|
|
||||||
end_date = end_str if end_str else date.today().isoformat()
|
|
||||||
|
|
||||||
agents = list(
|
|
||||||
Session.objects.values_list("agent_name", flat=True)
|
|
||||||
.distinct()
|
|
||||||
.order_by("agent_name")
|
|
||||||
)
|
|
||||||
|
|
||||||
sessions_qs = Session.objects.filter(
|
|
||||||
start_time__date__gte=start_date,
|
|
||||||
start_time__date__lte=end_date,
|
|
||||||
).order_by("start_time")
|
|
||||||
|
|
||||||
if agent_filter:
|
|
||||||
sessions_qs = sessions_qs.filter(agent_name=agent_filter)
|
|
||||||
|
|
||||||
messages_prefetch = Prefetch(
|
|
||||||
"messages",
|
|
||||||
queryset=Message.objects.order_by("seq"),
|
|
||||||
)
|
|
||||||
sessions_qs = sessions_qs.prefetch_related(messages_prefetch)
|
|
||||||
|
|
||||||
role_labels = {
|
|
||||||
"user": "User",
|
|
||||||
"assistant": "Assistant",
|
|
||||||
"toolResult": "Tool Result",
|
|
||||||
}
|
|
||||||
|
|
||||||
session_list = []
|
|
||||||
for session in sessions_qs:
|
|
||||||
messages = []
|
|
||||||
for msg in session.messages.all():
|
|
||||||
messages.append({
|
|
||||||
"timestamp": msg.timestamp,
|
|
||||||
"role": msg.role,
|
|
||||||
"content_text": msg.content_text,
|
|
||||||
"tool_name": msg.tool_name,
|
|
||||||
"get_role_label": role_labels.get(msg.role, msg.role),
|
|
||||||
})
|
|
||||||
session_list.append({
|
|
||||||
"session_id": session.session_id,
|
|
||||||
"agent_name": session.agent_name,
|
|
||||||
"model_id": session.model_id,
|
|
||||||
"total_tokens": session.total_tokens,
|
|
||||||
"start_time": session.start_time,
|
|
||||||
"messages": messages,
|
|
||||||
})
|
|
||||||
|
|
||||||
context = {
|
|
||||||
**admin.site.each_context(request),
|
|
||||||
"start_date": start_date,
|
|
||||||
"end_date": end_date,
|
|
||||||
"selected_agent": agent_filter,
|
|
||||||
"agents": agents,
|
|
||||||
"sessions": session_list,
|
|
||||||
"title": "Daily Conversation View",
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, "admin/openclaw/daily_view.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="Export selected sessions to Markdown")
|
@admin.action(description="Export selected sessions to Markdown")
|
||||||
def export_to_markdown(modeladmin, request, queryset):
|
def export_to_markdown(modeladmin, request, queryset):
|
||||||
md, filename = export_daily_markdown(queryset)
|
md, filename = export_daily_markdown(queryset)
|
||||||
@@ -140,7 +71,13 @@ class SessionAdmin(admin.ModelAdmin):
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
custom_urls = [
|
custom_urls = [
|
||||||
path("daily/", admin.site.admin_view(daily_conversation_view), name="openclaw_daily"),
|
path("daily/", admin.site.admin_view(daily_report_list_view), name="openclaw_daily"),
|
||||||
|
path("daily-reports/", admin.site.admin_view(daily_report_list_view), name="openclaw_daily_reports"),
|
||||||
|
path(
|
||||||
|
"daily-reports/<str:agent_name>/<int:year>-<int:month>-<int:day>/",
|
||||||
|
admin.site.admin_view(daily_report_detail_view),
|
||||||
|
name="openclaw_daily_report_detail",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
return custom_urls + urls
|
return custom_urls + urls
|
||||||
|
|
||||||
|
|||||||
123
src/openclaw/admin_new_views.py
Normal file
123
src/openclaw/admin_new_views.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
Daily report views for admin.
|
||||||
|
List: (agent, date) combos with message/session counts.
|
||||||
|
Detail: all messages for a specific agent on a specific date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.db.models.functions import TruncDate
|
||||||
|
from django.http import Http404
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
|
||||||
|
from openclaw.models import Session, Message, ToolCall
|
||||||
|
|
||||||
|
|
||||||
|
def daily_report_list_view(request):
|
||||||
|
"""
|
||||||
|
List page: shows all (agent_name, date) combinations with message/session counts.
|
||||||
|
Filterable by agent and date range.
|
||||||
|
"""
|
||||||
|
start_str = request.GET.get("start")
|
||||||
|
end_str = request.GET.get("end")
|
||||||
|
agent_filter = request.GET.get("agent", "")
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
start_date = start_str if start_str else (today.replace(day=1)).isoformat()
|
||||||
|
end_date = end_str if end_str else today.isoformat()
|
||||||
|
|
||||||
|
agents = list(
|
||||||
|
Session.objects.values_list("agent_name", flat=True)
|
||||||
|
.distinct()
|
||||||
|
.order_by("agent_name")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Query messages within range, select_related session for efficiency
|
||||||
|
messages_qs = Message.objects.filter(
|
||||||
|
timestamp__date__gte=start_date,
|
||||||
|
timestamp__date__lte=end_date,
|
||||||
|
).select_related("session").order_by("timestamp")
|
||||||
|
|
||||||
|
if agent_filter:
|
||||||
|
messages_qs = messages_qs.filter(session__agent_name=agent_filter)
|
||||||
|
|
||||||
|
# Group by (agent_name, date) in Python — simple and reliable
|
||||||
|
groups = defaultdict(lambda: {"message_count": 0, "session_ids": set()})
|
||||||
|
for msg in messages_qs:
|
||||||
|
d = msg.timestamp.date()
|
||||||
|
key = (msg.session.agent_name, d)
|
||||||
|
groups[key]["message_count"] += 1
|
||||||
|
groups[key]["session_ids"].add(msg.session.session_id)
|
||||||
|
|
||||||
|
# Build sorted list (newest first, then by agent)
|
||||||
|
groups_list = sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"agent_name": key[0],
|
||||||
|
"date_val": key[1],
|
||||||
|
"message_count": data["message_count"],
|
||||||
|
"session_count": len(data["session_ids"]),
|
||||||
|
}
|
||||||
|
for key, data in groups.items()
|
||||||
|
],
|
||||||
|
key=lambda x: (-x["date_val"].toordinal(), x["agent_name"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**admin.site.each_context(request),
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
"selected_agent": agent_filter,
|
||||||
|
"agents": agents,
|
||||||
|
"date_groups": groups_list,
|
||||||
|
"title": "Daily Report",
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "admin/openclaw/daily_report_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def daily_report_detail_view(request, agent_name, year, month, day):
|
||||||
|
"""
|
||||||
|
Detail page: all messages for a specific agent on a specific date.
|
||||||
|
"""
|
||||||
|
target_date = date(int(year), int(month), int(day))
|
||||||
|
|
||||||
|
sessions = (
|
||||||
|
Session.objects.filter(agent_name=agent_name)
|
||||||
|
.prefetch_related("messages", "messages__tool_calls")
|
||||||
|
.order_by("start_time")
|
||||||
|
)
|
||||||
|
|
||||||
|
sessions_with_messages = []
|
||||||
|
for session in sessions:
|
||||||
|
day_messages = session.messages.filter(
|
||||||
|
timestamp__date=target_date
|
||||||
|
).order_by("seq")
|
||||||
|
if day_messages.exists():
|
||||||
|
sessions_with_messages.append({
|
||||||
|
"session": session,
|
||||||
|
"messages": list(day_messages),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not sessions_with_messages:
|
||||||
|
raise Http404(f"No messages found for agent '{agent_name}' on {target_date}")
|
||||||
|
|
||||||
|
role_label_map = {
|
||||||
|
"user": "User",
|
||||||
|
"assistant": "Assistant",
|
||||||
|
"tool": "Tool",
|
||||||
|
"toolResult": "Tool Result",
|
||||||
|
"system": "System",
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**admin.site.each_context(request),
|
||||||
|
"agent_name": agent_name,
|
||||||
|
"target_date": target_date,
|
||||||
|
"sessions": sessions_with_messages,
|
||||||
|
"role_label_map": role_label_map,
|
||||||
|
"title": f"Daily Report: {agent_name} - {target_date}",
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "admin/openclaw/daily_report_detail.html", context)
|
||||||
119
src/openclaw/templates/admin/openclaw/daily_report_detail.html
Normal file
119
src/openclaw/templates/admin/openclaw/daily_report_detail.html
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | OpenClaw Archive{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1>📋 Daily Report: <span class="text-primary">{{ agent_name }}</span> — <span class="text-success">{{ target_date }}</span></h1>
|
||||||
|
<a href="{% url 'admin:openclaw_daily_reports' %}?agent={{ agent_name }}&start={{ target_date }}&end={{ target_date }}" class="btn btn-secondary">← Back to List</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for item in sessions %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-dark text-white">
|
||||||
|
<strong>Session:</strong> {{ item.session.session_id }}
|
||||||
|
|
|
||||||
|
<strong>Model:</strong> {{ item.session.model_id|default:"N/A" }}
|
||||||
|
|
|
||||||
|
<strong>Tokens:</strong> {{ item.session.total_tokens|default:0|intcomma }}
|
||||||
|
|
|
||||||
|
<strong>Cost:</strong> ${{ item.session.total_cost|default:0|floatformat:4 }}
|
||||||
|
|
|
||||||
|
<strong>Time:</strong> {{ item.session.start_time|date:"Y-m-d H:i" }} ~ {{ item.session.end_time|date:"H:i"|default:"Ongoing" }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-bordered table-sm mb-0" style="font-size:12px; table-layout: fixed;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:60px">
|
||||||
|
<col style="width:100px">
|
||||||
|
<col style="width:80px">
|
||||||
|
<col style="width:140px">
|
||||||
|
<col style="width:500px">
|
||||||
|
</colgroup>
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Seq</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Tool / Meta</th>
|
||||||
|
<th>Content</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for msg in item.messages %}
|
||||||
|
<tr style="vertical-align: top;">
|
||||||
|
<td><span class="badge bg-secondary">{{ msg.seq }}</span></td>
|
||||||
|
<td><small>{{ msg.timestamp|date:"H:i:s" }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if msg.role == "user" %}
|
||||||
|
<span class="badge bg-primary">User</span>
|
||||||
|
{% elif msg.role == "assistant" %}
|
||||||
|
<span class="badge bg-success">Assistant</span>
|
||||||
|
{% elif msg.role == "toolResult" %}
|
||||||
|
<span class="badge bg-warning text-dark">Tool Result</span>
|
||||||
|
{% elif msg.role == "tool" %}
|
||||||
|
<span class="badge bg-info text-dark">Tool</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark">{{ msg.role }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if msg.tool_name %}
|
||||||
|
<div><small><strong>Tool:</strong> <code>{{ msg.tool_name }}</code></small></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if msg.exit_code != None %}
|
||||||
|
<div><small><strong>Exit:</strong> {{ msg.exit_code }}</small></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if msg.duration_ms %}
|
||||||
|
<div><small><strong>Duration:</strong> {{ msg.duration_ms }}ms</small></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if msg.tokens_total > 0 %}
|
||||||
|
<div><small><strong>Tokens:</strong> {{ msg.tokens_total|intcomma }}</small></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if msg.is_error %}
|
||||||
|
<div><span class="badge bg-danger">Error</span></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if msg.content_text %}
|
||||||
|
<pre style="font-size:11px; white-space: pre-wrap; word-break: break-word; margin:0; max-height: 300px; overflow-y: auto; background: #f9f9f9; padding: 4px;">{{ msg.content_text|truncatechars:2000 }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted"><em>(empty)</em></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Tool calls for this message #}
|
||||||
|
{% with msg.tool_calls.all as tc_list %}
|
||||||
|
{% if tc_list %}
|
||||||
|
<div class="mt-2">
|
||||||
|
{% for tc in tc_list %}
|
||||||
|
<div class="border rounded p-1 mb-1 bg-light">
|
||||||
|
<small><strong>TC:</strong> {{ tc.tool_name }} ({{ tc.tool_call_id|truncatechars:20 }})</small>
|
||||||
|
{% if tc.arguments %}
|
||||||
|
<details>
|
||||||
|
<summary><small>Arguments</small></summary>
|
||||||
|
<pre style="font-size:10px; white-space: pre-wrap; word-break: break-word; margin:4px 0 0; background:#fff; border:1px solid #ddd; padding:4px;">{{ tc.arguments|pprint }}</pre>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
{% if tc.result_text %}
|
||||||
|
<details>
|
||||||
|
<summary><small>Result ({{ tc.result_text|length }} chars)</small></summary>
|
||||||
|
<pre style="font-size:10px; white-space: pre-wrap; word-break: break-word; margin:4px 0 0; background:#fff; border:1px solid #ddd; padding:4px; max-height:200px; overflow-y:auto;">{{ tc.result_text|truncatechars:2000 }}</pre>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
72
src/openclaw/templates/admin/openclaw/daily_report_list.html
Normal file
72
src/openclaw/templates/admin/openclaw/daily_report_list.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | OpenClaw Archive{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:"Django Admin" }}</a></h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="p-3">
|
||||||
|
<h1>📅 Daily Report</h1>
|
||||||
|
|
||||||
|
<form method="get" class="mb-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>Agent</label>
|
||||||
|
<select name="agent" class="form-control">
|
||||||
|
<option value="">All Agents</option>
|
||||||
|
{% for agent in agents %}
|
||||||
|
<option value="{{ agent }}" {% if agent == selected_agent %}selected{% endif %}>{{ agent }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>Start Date</label>
|
||||||
|
<input type="date" name="start" value="{{ start_date }}" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>End Date</label>
|
||||||
|
<input type="date" name="end" value="{{ end_date }}" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Filter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if date_groups %}
|
||||||
|
<table class="table table-bordered table-sm" style="font-size:13px">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Agent</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
<th>Messages</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for group in date_groups %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ group.agent_name }}</strong></td>
|
||||||
|
<td>{{ group.date_val }}</td>
|
||||||
|
<td>{{ group.session_count }}</td>
|
||||||
|
<td>{{ group.message_count }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'admin:openclaw_daily_report_detail' group.agent_name group.date_val.year group.date_val.month group.date_val.day %}"
|
||||||
|
class="btn btn-sm btn-info">
|
||||||
|
View Report
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No messages found for the selected date range.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user