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:
ishenwei
2026-04-08 19:25:07 +08:00
parent 1a32801e39
commit 49d772139d
5 changed files with 323 additions and 71 deletions

View File

@@ -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
View 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

View 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)

View 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 }}
&nbsp;|&nbsp;
<strong>Model:</strong> {{ item.session.model_id|default:"N/A" }}
&nbsp;|&nbsp;
<strong>Tokens:</strong> {{ item.session.total_tokens|default:0|intcomma }}
&nbsp;|&nbsp;
<strong>Cost:</strong> ${{ item.session.total_cost|default:0|floatformat:4 }}
&nbsp;|&nbsp;
<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 %}

View 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 %}