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.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.humanize",
|
||||
"rest_framework",
|
||||
"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.db.models import Prefetch
|
||||
from django.http import HttpResponse
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from openclaw.export import export_daily_markdown
|
||||
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):
|
||||
@@ -35,73 +33,6 @@ class ToolCallInline(admin.TabularInline):
|
||||
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")
|
||||
def export_to_markdown(modeladmin, request, queryset):
|
||||
md, filename = export_daily_markdown(queryset)
|
||||
@@ -140,7 +71,13 @@ class SessionAdmin(admin.ModelAdmin):
|
||||
from django.urls import path
|
||||
urls = super().get_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
|
||||
|
||||
|
||||
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