From 16a4f01969951ee4a479351e1b84f7dd369a8a6d Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 9 Oct 2026 16:26:39 +0800 Subject: [PATCH 1/8] feat: expose admin user agent activity --- dashboard/backend/api/routers/admin_users.py | 35 ++++++ .../tests/test_admin_agent_activity_api.py | 58 ++++++++++ .../backend/tests/test_app_composition.py | 1 + .../2026-10-09-user-agent-trace-navigation.md | 100 ++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 dashboard/backend/tests/test_admin_agent_activity_api.py create mode 100644 docs/superpowers/plans/2026-10-09-user-agent-trace-navigation.md diff --git a/dashboard/backend/api/routers/admin_users.py b/dashboard/backend/api/routers/admin_users.py index 6847f662c..48ee2f086 100644 --- a/dashboard/backend/api/routers/admin_users.py +++ b/dashboard/backend/api/routers/admin_users.py @@ -228,6 +228,41 @@ def get_user(user_id: int, _admin: dict = Depends(require_admin)): return {"user": payload} +@admin_router.get("/users/{user_id}/agent-activity") +def get_user_agent_activity(user_id: int, _admin: dict = Depends(require_admin)): + """Return the admin projection from a user to traceable agent runs.""" + if users_module.user_store.get_user_admin(user_id) is None: + raise HTTPException(status_code=404, detail="User not found") + + # Agent content and protocol runs use separate persistence boundaries, so + # join them in Python by stable IDs. An older run without a trace remains + # visible instead of disappearing from the operator's history. + from dashboard.backend.domain.agents.repository import agent_store + from dashboard.backend.domain.runs.repository import run_store + from dashboard.backend.domain.traces.repository import trace_store + + activity = [] + for agent in agent_store.list_agents(owner_user_id=user_id): + runs = [] + for run in run_store.list_runs(agent["agent_id"]): + trace = trace_store.get_trace_for_run(run["run_id"]) + runs.append({ + "run_id": run["run_id"], + "status": run.get("status"), + "created_at": run.get("created_at"), + "trace_id": trace.get("trace_id") if trace else None, + "trace_status": trace.get("status") if trace else None, + }) + activity.append({ + "agent_id": agent["agent_id"], + "name": agent.get("name") or agent["agent_id"], + "agent_type": agent.get("agent_type") or "external", + "created_at": agent.get("created_at"), + "runs": runs, + }) + return {"user_id": user_id, "agents": activity} + + @admin_router.patch("/users/{user_id}") def patch_user( user_id: int, diff --git a/dashboard/backend/tests/test_admin_agent_activity_api.py b/dashboard/backend/tests/test_admin_agent_activity_api.py new file mode 100644 index 000000000..170b6eac9 --- /dev/null +++ b/dashboard/backend/tests/test_admin_agent_activity_api.py @@ -0,0 +1,58 @@ +from fastapi.testclient import TestClient + +from dashboard.backend.api.auth import require_admin +from dashboard.backend.app import app + + +def test_admin_user_agent_activity_joins_agents_runs_and_traces(monkeypatch): + import dashboard.backend.api.routers.admin_users as module + import dashboard.backend.domain.agents.repository as agents + import dashboard.backend.domain.runs.repository as runs + import dashboard.backend.domain.traces.repository as traces + + class Users: + def get_user_admin(self, user_id): + return {"id": user_id} if user_id == 7 else None + + class Agents: + def list_agents(self, *, owner_user_id): + assert owner_user_id == 7 + return [{"agent_id": "agent-1", "name": "Momentum", "agent_type": "builtin", "created_at": "t1"}] + + class Runs: + def list_runs(self, agent_id): + assert agent_id == "agent-1" + return [{"run_id": "run-1", "status": "running", "created_at": "t2"}] + + class Traces: + def get_trace_for_run(self, run_id): + assert run_id == "run-1" + return {"trace_id": "trace-1", "status": "running"} + + monkeypatch.setattr(module.users_module, "user_store", Users()) + monkeypatch.setattr(agents, "agent_store", Agents()) + monkeypatch.setattr(runs, "run_store", Runs()) + monkeypatch.setattr(traces, "trace_store", Traces()) + app.dependency_overrides[require_admin] = lambda: {"id": 1, "is_admin": True} + try: + response = TestClient(app).get("/api/admin/users/7/agent-activity") + assert response.status_code == 200 + assert response.json()["agents"][0]["runs"][0]["trace_id"] == "trace-1" + finally: + app.dependency_overrides.pop(require_admin, None) + + +def test_admin_user_agent_activity_returns_404_for_unknown_user(monkeypatch): + import dashboard.backend.api.routers.admin_users as module + + class Users: + def get_user_admin(self, _user_id): + return None + + monkeypatch.setattr(module.users_module, "user_store", Users()) + app.dependency_overrides[require_admin] = lambda: {"id": 1, "is_admin": True} + try: + response = TestClient(app).get("/api/admin/users/999/agent-activity") + assert response.status_code == 404 + finally: + app.dependency_overrides.pop(require_admin, None) diff --git a/dashboard/backend/tests/test_app_composition.py b/dashboard/backend/tests/test_app_composition.py index b3975d846..4a75e3452 100644 --- a/dashboard/backend/tests/test_app_composition.py +++ b/dashboard/backend/tests/test_app_composition.py @@ -141,6 +141,7 @@ ("GET", "/api/admin/stats"), ("GET", "/api/admin/users"), ("GET", "/api/admin/users/{user_id}"), + ("GET", "/api/admin/users/{user_id}/agent-activity"), ("PATCH", "/api/admin/users/{user_id}"), ("GET", "/api/admin/analytics/overview"), ("GET", "/api/admin/analytics/lifecycle"), diff --git a/docs/superpowers/plans/2026-10-09-user-agent-trace-navigation.md b/docs/superpowers/plans/2026-10-09-user-agent-trace-navigation.md new file mode 100644 index 000000000..915335e2b --- /dev/null +++ b/docs/superpowers/plans/2026-10-09-user-agent-trace-navigation.md @@ -0,0 +1,100 @@ +# User Agent Trace Navigation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (inline execution). Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Add an Admin navigation path from User Analytics to a user's agents, runs, and live reasoning trace. + +**Architecture:** Reuse the existing admin user profile and trace API. Add a server-owned user-to-agent/run summary endpoint, then add profile-level links that navigate to the existing trace list/detail views. Keep trace storage and event contracts unchanged. + +**Tech Stack:** FastAPI, existing Agent/Run repositories, vanilla JavaScript Admin modules, pytest and the repository DOM harness. + +**Spec:** `docs/superpowers/specs/2026-10-07-agent-reasoning-trace-design.md` + +## Global Constraints + +- Keep trace data in the existing Agent Postgres/Neon boundary. +- Do not expose hidden chain-of-thought; display bounded reasoning summaries only. +- Reuse `require_admin`, existing AdminShell request/routing, and textContent-based DOM rendering. +- Preserve existing user analytics pagination and profile section behavior. +- Every loop ends with focused tests, a commit, and a push to PR #622. +- Do not add a second database or duplicate trace storage. + +--- + +### Task 1: Define the Admin user-agent-run contract + +**Files:** +- Create: `dashboard/backend/api/routers/admin_agent_activity.py` +- Modify: `dashboard/backend/api/router.py` +- Test: `dashboard/backend/tests/test_admin_agent_activity_api.py` +- Modify: `dashboard/backend/tests/test_app_composition.py` + +**Interfaces:** +- `GET /api/admin/users/{user_id}/agent-activity` +- Response: `{user_id, agents: [{agent_id, name, agent_type, created_at, runs: [{run_id, status, created_at, trace_id}]}]}` +- Endpoint is admin-only and returns empty arrays for a valid user with no agents. + +- [ ] Add authorization, empty-user, owned-agent, run-link, and missing-trace tests. +- [ ] Run the new API test and confirm it fails because the route is absent. +- [ ] Implement repository-backed aggregation using existing agent and run stores; resolve trace IDs through `trace_store.get_trace_for_run`. +- [ ] Register the router and route contract. +- [ ] Run focused API and route-composition tests. +- [ ] Commit `feat: expose admin user agent activity`. +- [ ] Push the branch and verify PR checks start. + +### Task 2: Render agent and run links in User Analytics profiles + +**Files:** +- Modify: `dashboard/frontend/js/admin-users.js` +- Modify: `dashboard/frontend/admin.css` +- Test: `dashboard/backend/tests/test_admin_users_frontend.py` + +**Interfaces:** +- The user profile gets an “Agent activity” section. +- Each agent displays name/id and its runs. +- A run with a trace links to `#traces/`. +- Running traces use copy such as “View live trace”; terminal traces use “View trace”. +- Loading, empty, error, and stale states remain explicit. + +- [ ] Add DOM tests for empty activity, agent rows, trace links, and safe special-character rendering. +- [ ] Run focused frontend tests and confirm failure before implementation. +- [ ] Add lazy loading on profile open/section selection via AdminShell. +- [ ] Render all labels using DOM nodes/textContent. +- [ ] Run focused frontend and existing Admin shell tests. +- [ ] Commit `feat: link user analytics to agent traces`. +- [ ] Push and verify PR checks. + +### Task 3: Add trace context and back navigation + +**Files:** +- Modify: `dashboard/frontend/js/admin-traces.js` +- Modify: `dashboard/backend/tests/test_admin_traces_frontend.py` + +**Interfaces:** +- Trace detail accepts optional `user_id`, `agent_id`, and `run_id` context in the hash query. +- Detail breadcrumb includes `Back to user analytics` when `user_id` is present. +- Existing direct `#traces` and `#traces/` behavior remains unchanged. + +- [ ] Add DOM tests for contextual breadcrumb and query-preserving navigation. +- [ ] Implement context-safe links and breadcrumb. +- [ ] Run frontend trace tests and route tests. +- [ ] Commit `feat: add user context to trace navigation`. +- [ ] Push and verify PR checks. + +### Task 4: Verify the complete operator flow + +**Files:** +- Modify: `dashboard/backend/tests/test_admin_user_trace_flow.py` +- Modify: `docs/superpowers/specs/2026-10-07-agent-reasoning-trace-design.md` + +**Interfaces:** +- Contract test covers User -> Agent -> Run -> Trace. +- Documentation records the Admin entry point and near-real-time polling behavior. + +- [ ] Add an API contract test with a user, agent, run, trace, and terminal event. +- [ ] Run the focused flow, trace, and Admin suites. +- [ ] Run `git diff --check` and frontend syntax checks. +- [ ] Update documentation with the navigation path. +- [ ] Commit `docs: document admin trace navigation`. +- [ ] Push and report local and remote CI status. + From 083901175fb929221263f00c77ed93327ab44343 Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 9 Oct 2026 16:40:14 +0800 Subject: [PATCH 2/8] feat: link user analytics to agent traces --- .../tests/test_admin_users_frontend.py | 2 +- dashboard/frontend/js/admin-users.js | 38 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/dashboard/backend/tests/test_admin_users_frontend.py b/dashboard/backend/tests/test_admin_users_frontend.py index 0fb772903..4ca5fdc00 100644 --- a/dashboard/backend/tests/test_admin_users_frontend.py +++ b/dashboard/backend/tests/test_admin_users_frontend.py @@ -128,7 +128,7 @@ def test_profile_header_carries_badges_activation_week_and_milestones(): "Account signed upJul 1, 2026, 09:00 UTC", "Credential verifiedJul 1, 2026, 10:00 UTC", "Agent createdJul 1, 2026, 11:00 UTC", "Backtest completedJul 2, 2026, 10:00 UTC", ] - assert result["tabs"] == ["Overview", "Timeline", "Runs", "Usage", "Sessions"] + assert result["tabs"] == ["Overview", "Agents & traces", "Timeline", "Runs", "Usage", "Sessions"] # D5: account management lives on this page; the hand-off rides in the # hash query the shell parses and AdminCredits applies to its search form. assert "#account?user=ada.synthetic%40example.test" in result["account"] diff --git a/dashboard/frontend/js/admin-users.js b/dashboard/frontend/js/admin-users.js index 0cd76fbfa..ab65843e6 100644 --- a/dashboard/frontend/js/admin-users.js +++ b/dashboard/frontend/js/admin-users.js @@ -4,8 +4,8 @@ const USERS_PATH = '/api/admin/analytics/users'; const PAGE_SIZE = 50; - const SECTIONS = ['overview', 'timeline', 'runs', 'usage', 'sessions']; - const SECTION_LABELS = Object.freeze({ overview: 'Overview', timeline: 'Timeline', runs: 'Runs', usage: 'Usage', sessions: 'Sessions' }); + const SECTIONS = ['overview', 'agents', 'timeline', 'runs', 'usage', 'sessions']; + const SECTION_LABELS = Object.freeze({ overview: 'Overview', agents: 'Agents & traces', timeline: 'Timeline', runs: 'Runs', usage: 'Usage', sessions: 'Sessions' }); // Harvested from the retired admin-analytics.js:35-50 (deleted in PR C; see git history). const EVENT_LABELS = Object.freeze({ account_signed_up: 'Account signed up', @@ -446,6 +446,31 @@ const s = shell(); const rows = Array.isArray(items) ? items : []; if (!rows.length) return s.el('p', 'panel-empty', 'No activity in this section.'); + if (section === 'agents') { + const root = s.el('div', 'agent-activity-list'); + rows.forEach((agent) => { + const card = s.el('article', 'agent-activity-card'); + card.appendChild(s.el('h3', '', agent.name || agent.agent_id || s.DASH)); + card.appendChild(s.el('p', 'muted', `${agent.agent_type || 'external'} · ${agent.agent_id || s.DASH}`)); + const runs = s.el('ul', 'agent-run-list'); + (agent.runs || []).forEach((run) => { + const item = s.el('li'); + item.appendChild(s.el('span', '', `${run.run_id || s.DASH} · ${s.humanize(run.status || 'unknown')}`)); + if (run.trace_id) { + const link = s.el('a', 'module-link', run.trace_status === 'running' ? 'View live trace' : 'View trace'); + link.setAttribute('href', `#traces/${encodeURIComponent(String(run.trace_id))}?user_id=${encodeURIComponent(String(state.profile.userId))}&agent_id=${encodeURIComponent(String(agent.agent_id))}&run_id=${encodeURIComponent(String(run.run_id))}`); + item.appendChild(link); + } else { + item.appendChild(s.el('span', 'muted', 'Trace unavailable')); + } + runs.appendChild(item); + }); + if (!runs.children.length) runs.appendChild(s.el('li', 'panel-empty', 'No runs recorded.')); + card.appendChild(runs); + root.appendChild(card); + }); + return root; + } if (section === 'timeline') { const list = s.el('ol', 'timeline'); rows.forEach((item) => { @@ -527,12 +552,15 @@ sectionState.error = null; const seq = ++sectionState.requestSeq; renderSectionPanel(section); - const params = new URLSearchParams({ section, limit: String(PAGE_SIZE) }); + const params = new URLSearchParams({ section, limit: String(PAGE_SIZE) }); if (append) params.set('cursor', sectionState.nextCursor); try { - const payload = await s.request(`${USERS_PATH}/${encodeURIComponent(String(userId))}/activity?${params}`); + const path = section === 'agents' + ? `/api/admin/users/${encodeURIComponent(String(userId))}/agent-activity` + : `${USERS_PATH}/${encodeURIComponent(String(userId))}/activity?${params}`; + const payload = await s.request(path); if (String(state.profile.userId) !== String(userId) || seq !== sectionState.requestSeq) return; - const next = Array.isArray(payload.items) ? payload.items : []; + const next = section === 'agents' ? (Array.isArray(payload.agents) ? payload.agents : []) : (Array.isArray(payload.items) ? payload.items : []); sectionState.items = append ? sectionState.items.concat(next) : next; sectionState.nextCursor = payload.next_cursor || null; sectionState.loaded = true; From 8956f45b5657bf2ceae2371858b051541528e150 Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 9 Oct 2026 16:56:56 +0800 Subject: [PATCH 3/8] feat: add user context to trace navigation --- dashboard/frontend/js/admin-traces.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dashboard/frontend/js/admin-traces.js b/dashboard/frontend/js/admin-traces.js index 79f5840cd..cfbd88c79 100644 --- a/dashboard/frontend/js/admin-traces.js +++ b/dashboard/frontend/js/admin-traces.js @@ -5,7 +5,7 @@ const SURFACE = 'traces'; const state = { traceId: null, trace: null, events: [], nextSequence: 0, - pollTimer: null, stale: false, listItems: [], listHasMore: false, listNextCursor: 0, + pollTimer: null, stale: false, context: {}, listItems: [], listHasMore: false, listNextCursor: 0, }; function shell() { @@ -133,12 +133,12 @@ return section; } - function renderTraceDetail(trace, events) { + function renderTraceDetail(trace, events, context = {}) { const section = node('section', 'trace-detail-panel'); const breadcrumb = node('nav', 'breadcrumb'); breadcrumb.setAttribute('aria-label', 'Breadcrumb'); - const back = node('a', '', 'Agent traces'); - back.setAttribute('href', '#traces'); + const back = node('a', '', context.user_id ? 'Back to user analytics' : 'Agent traces'); + back.setAttribute('href', context.user_id ? `#users/${encodeURIComponent(String(context.user_id))}` : '#traces'); breadcrumb.append(back, node('span', '', '/'), node('span', '', value(trace?.trace_id))); section.appendChild(breadcrumb); const head = node('div', 'page-head'); @@ -228,7 +228,7 @@ return shell().request(`/api/admin/traces/${encodeURIComponent(traceId)}`); } - async function loadDetail(traceId) { + async function loadDetail(traceId, context = {}) { stopPolling(); const seq = shell().nextSeq(SURFACE); try { @@ -238,6 +238,7 @@ if (terminal) trace = await refreshTraceEnvelope(traceId); if (!shell().isCurrent(SURFACE, seq)) return; state.traceId = traceId; + state.context = { ...context }; state.trace = trace; state.events = events; state.nextSequence = state.events.length ? state.events[state.events.length - 1].sequence_no : 0; @@ -245,7 +246,7 @@ const host = element('tracesView'); if (!host) return; shell().clear(host); - host.appendChild(renderTraceDetail(trace, state.events)); + host.appendChild(renderTraceDetail(trace, state.events, context)); if (trace.status === 'running') startPolling(); } catch (error) { if (await shell().handleAccessLost(error)) return; @@ -275,7 +276,7 @@ const host = element('tracesView'); if (host && state.trace) { shell().clear(host); - host.appendChild(renderTraceDetail(state.trace, state.events)); + host.appendChild(renderTraceDetail(state.trace, state.events, state.context)); } } state.stale = false; @@ -296,7 +297,7 @@ function onRoute(event) { if (event.detail.route !== SURFACE) return; - if (event.detail.id) loadDetail(event.detail.id); + if (event.detail.id) loadDetail(event.detail.id, event.detail.query || {}); else loadList(); } From 7de1ef9c3892c500f9036a60900401351ee9294c Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 9 Oct 2026 17:03:05 +0800 Subject: [PATCH 4/8] docs: document admin trace navigation --- .../2026-10-07-agent-reasoning-trace-design.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-10-07-agent-reasoning-trace-design.md b/docs/superpowers/specs/2026-10-07-agent-reasoning-trace-design.md index 7e86c90da..eb7e86861 100644 --- a/docs/superpowers/specs/2026-10-07-agent-reasoning-trace-design.md +++ b/docs/superpowers/specs/2026-10-07-agent-reasoning-trace-design.md @@ -66,6 +66,23 @@ but remain physically co-located so Admin queries do not require cross-database joins. A later split is allowed only after measured volume or retention needs justify it. +### Admin navigation + +The operator path is layered rather than a second trace surface: + +```text +User Analytics -> User profile -> Agents & traces -> Run -> Reasoning trace +``` + +The profile's `Agents & traces` section reads the admin-only +`GET /api/admin/users/{user_id}/agent-activity` projection. It joins Agent, +Run, and Trace records by stable IDs in the application layer because those +records may live in different persistence boundaries. A run without a trace +remains visible and is labeled as unavailable. Runs with a running trace link +to the polling timeline, while completed and failed runs link to the same +timeline in read-only mode. The global `Agent traces` Admin entry remains the +cross-user operational view. + ## Data model ### `agent_traces` @@ -262,4 +279,3 @@ verification -> regression check -> short review note. - Exact retention duration and archival schedule. - SSE versus WebSocket after polling proves the event contract. - Additional event types for LLM provider requests and child Agents. - From ef19ce253d041b24f92cd3679f8eb4584d086f47 Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 9 Oct 2026 19:03:00 +0800 Subject: [PATCH 5/8] fix(admin): differentiate agent traces icon --- dashboard/frontend/admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/frontend/admin.html b/dashboard/frontend/admin.html index c3c39db4d..e8bc079b2 100644 --- a/dashboard/frontend/admin.html +++ b/dashboard/frontend/admin.html @@ -162,7 +162,7 @@

Agentic Trading Lab

Account Management Providers Activity - Agent traces + Agent traces