Skip to content

Dashboard routing breaks on non-root mounts (README's own /admin/agents), plus dead benchmarks route and host-absolute chrome #392

Description

@TonsOfFun

Part of an ActiveAgent + actionagent dashboard functional review (multi-agent, adversarially verified). Severity: 🟠 Major.

Dashboard.jsx parses window.location.pathname directly and never strips the engine mount prefix, so any mount whose prefix contains a view keyword — e.g. the README's own /admin/agents — misroutes deep links. The engine also retains dead benchmarks routing (a view it neither imports nor has a backend for), and ships app-only host-absolute chrome: Header sign-out POSTs to /session (404 on a generic host), AgentRunner hardcodes /pricing, and the quota banner's "See plans" link bypasses the engine's documented upgrade_url.

Findings

Engine route parser matches the raw pathname, so any mount whose prefix contains a view keyword (README's own /admin/agents) misroutes deep links

  • Where: actionagent/frontend/pages/Dashboard.jsx:49 · severity: major · kind: bug · repo: activeagent
  • What breaks: actionagent/frontend/pages/Dashboard.jsx applyPath() matches window.location.pathname directly with path.includes()/regexes and never strips the engine mount prefix (available as props.mountPath, used by dashboardPath.js for building URLs but not for parsing them). This works only when the mount contains no keyword. At mounts the README explicitly advertises this breaks: mounted at /admin/agents, the '/agents/' inside the mount prefix trips the !path.includes('/agents/') guards on the interactions and analytics branches, so /admin/agents/interactions and /admin/agents/analytics both fall through to the agent list instead of their views; deep links and browser back/forward land on the wrong screen. A mount whose prefix is itself a keyword (/admin/traces, /demo, /tools, /settings) is worse: every path resolves to that one view (e.g. mount /demo -> root renders the sandbox; mount /admin/traces -> everything renders Traces). The default /activeagents mount escapes only by luck ('activeagents' has no '/agents/' substring). The app copy is immune because it is hardwired to the fixed /dashboard mount.
  • Evidence: Code read end-to-end: applyPath (Dashboard.jsx:48-104) reads window.location.pathname (line 49) and matches without subtracting props.mountPath; guards at lines 54 and 65 use !path.includes('/agents/'). README.md:37-38 claims '/activeagents, /admin/agents and /dashboard all work'. Simulation of the exact branch chain: mount /admin/agents -> '/admin/agents/interactions' => 'list' (want interactions), '/admin/agents/analytics' => 'list' (want analytics); mount /demo -> '/demo' => 'sandbox' (want list); mount /admin/traces -> '/admin/traces/metrics' => 'traces' (want metrics). Default '/activeagents/interactions' => 'interactions' (works, no '/agents/' collision).
  • Suggested fix: In applyPath, strip the mount before matching: const mount = (window.ACTIVE_AGENT_DASHBOARD?.mountPath || '').replace(/\/$/, ''); let path = window.location.pathname; if (mount && path.startsWith(mount)) path = path.slice(mount.length) || '/'; then run every includes()/regex against the relative path (and anchor the simple keyword checks, e.g. path.startsWith('/traces'), so future collisions inside ids/query-free segments can't recur). Apply the same mount subtraction to any other component parsing window.location.pathname — notably AgentInteractions.jsx's applyLocation — ideally via a shared helper exported from utils/dashboardPath.js (the natural inverse of dashboardPath()).

Engine retains dead 'benchmarks' routing after extraction: URL + navigateTo target a view it neither imports, renders, nor has a backend for

  • Where: actionagent/frontend/pages/Dashboard.jsx:92 · severity: minor · kind: drift · repo: activeagent
  • What breaks: Benchmarks live only on the app side (app has BenchmarkView.jsx, Api::BenchmarksController, and resources :benchmarks in routes). During extraction the engine kept the benchmarks routing but not the feature: actionagent/frontend/pages/Dashboard.jsx sets currentView='benchmarks' for a /benchmarks URL (lines 92-93) and navigateTo maps a benchmarks path (line 286), but there is no BenchmarkView import and no case 'benchmarks' in renderContent, so the switch falls to default and silently renders the AgentList. There is also no benchmarks route/controller in the engine and no Sidebar entry, so it is only reachable by typing /benchmarks — which then shows the agent list instead of a 404 or a benchmarks screen. This is dangling drift; the engine either needs the BenchmarkView ported or the benchmarks routing removed.
  • Evidence: grep on engine Dashboard.jsx: 'NO import, NO render case (but route+navigateTo exist)' — lines 92-93 (applyPath benchmarks) and 286 (navigateTo benchmarks path) present; renderContent (lines 296-433) has no benchmarks case. grep benchmarks actionagent/config/routes.rb -> none; no benchmarks controller in actionagent/app/controllers. App has app/javascript/components/dashboard/BenchmarkView.jsx, Api::BenchmarksController, and routes.rb:178 resources :benchmarks.
  • Suggested fix: Remove the dead branch from the engine rather than porting: delete the else if (path.includes('/benchmarks')) { setCurrentView('benchmarks'); } branch (Dashboard.jsx:92-93) and the else if (view === 'benchmarks') path = dashboardPath('/benchmarks'); line (Dashboard.jsx:286), then rebuild the frontend bundle (the stale mapping is also baked into actionagent/app/assets/builds/action_agent.js). Porting BenchmarkView is the wrong direction — the app's benchmarks feature is platform-specific (ragents cloud benchmark runs ingested from bin/bench via Api::BenchmarksController), not something the mountable engine backs. Optionally also drop the now-unused benchmarks: '%%' icon entry in frontend/utils/designTokens.js:65.

Engine ships app-only host-absolute chrome: Header sign-out POSTs to /session (404 on a generic host) and AgentRunner hardcodes an

  • Where: actionagent/frontend/components/dashboard/Header.jsx:27 · severity: minor · kind: drift · repo: activeagent
  • What breaks: Header.jsx was extracted byte-for-byte from the app and hardcodes a sign-out that builds a form POST (with _method=delete) to the absolute host path /session. That route exists on the activeagents.ai platform but not on an arbitrary host that merely mounts the engine, so the visible 'Sign out' button in the engine dashboard posts to a nonexistent route. The fetch base-path shim in index.jsx only rewrites /api/ paths, so this host-level link is (correctly) left alone — but it points nowhere in a mounted engine. Similarly AgentRunner.jsx:210 hardcodes a 'See plans' link to /pricing, bypassing the configurable ActionAgent.upgrade_url that the adjacent 'Upgrade to Pro' button correctly uses via startCheckout. Both are platform assumptions leaked into the mountable engine.
  • Evidence: curl (dummy host, engine mounted at /activeagents): POST /session -> 404. Header.jsx:25-42 sets form.action='/session' and _method='delete' (identical to app copy; diff of the two Header.jsx exits 0). AgentRunner.jsx:210 <a href="/pricing">See plans</a> — a hardcoded platform path, while handleUpgrade() (line 183-192) correctly routes through startCheckout()/upgrade_url.
  • Suggested fix: In the engine only: (a) Header.jsx — drive sign-out from config (e.g. expose ActionAgent.sign_out_path via the meta blob dashboard_controller.rb already injects, alongside upgradeUrl) and hide the Sign out menu item when unset, instead of posting to '/session'; (b) AgentRunner.jsx:210 — replace the hardcoded href="/pricing" with window.ACTIVE_AGENT_DASHBOARD?.meta?.upgradeUrl (same source startCheckout uses) and render the 'See plans' link only when it is set. Also fix the stale comment in frontend/utils/checkout.js:7 which names the config as ActiveAgent::Dashboard.upgrade_url — the actual accessor is ActionAgent.upgrade_url. The app copies need no change (/pricing and /session exist there).

Quota banner's 'See plans' link hardcodes the platform's /pricing path, bypassing the engine's documented upgrade_url mechanism

  • Where: actionagent/frontend/components/dashboard/AgentRunner.jsx:210 · severity: minor · kind: drift · repo: activeagent
  • What breaks: AgentRunner's plan-limit banner (shown when a run POST is denied 402 by a host-configured quota_checker) renders See plans — an absolute host-root path inherited from the activeagents.ai platform, where GET /pricing exists. In the extracted engine no such route exists, so on any engine host that wires up quota_checker the link 404s. This contradicts the engine's own design: ActionAgent.upgrade_url is documented as 'Where the dashboard's upgrade CTAs should send people. Unset in a self-hosted install ... and the CTAs say so instead of linking nowhere' (actionagent/lib/action_agent.rb:231-235), and the adjacent 'Upgrade to Pro' button correctly goes through startCheckout()/meta.upgradeUrl (frontend/utils/checkout.js:12-18). The dead link ships in the prebuilt bundle ('See plans' present in app/assets/builds/action_agent.js).
  • Evidence: actionagent/frontend/components/dashboard/AgentRunner.jsx:210 hardcodes href="/pricing"; sibling app copy /home/user/activeagents/app/javascript/components/dashboard/AgentRunner.jsx:209 has the same link but the platform defines the route (activeagents config/routes.rb:27 'get "pricing", to: "pages#pricing"') — the engine's config/routes.rb has no pricing route. checkout.js and action_agent.rb:231-235 document the upgrade_url contract the link bypasses. grep 'See plans' app/assets/builds/action_agent.js confirms it ships.
  • Suggested fix: In AgentRunner.jsx, read const upgradeUrl = window.ACTIVE_AGENT_DASHBOARD?.meta?.upgradeUrl and render the 'See plans' anchor with href={upgradeUrl} only when it is set; when unset, omit the anchor (the banner's 'Upgrade to Pro' button already surfaces the 'This dashboard has no billing configured.' error via startCheckout, so no extra copy is required). Rebuild the frontend into app/assets/builds/action_agent.js.

Verification

Each finding above was produced by a dedicated per-feature review agent, then confirmed by an independent adversarial verifier (all rated high-confidence; zero rejected in this set). File:line citations are against the current main/HEAD of each repo; many were reproduced live against a booted dashboard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions