Skip to content

Make the dashboard engine (local dev console) work out of the box#344

Merged
TonsOfFun merged 4 commits into
mainfrom
claude/dashboard-production-ready
Jul 16, 2026
Merged

Make the dashboard engine (local dev console) work out of the box#344
TonsOfFun merged 4 commits into
mainfrom
claude/dashboard-production-ready

Conversation

@TonsOfFun

@TonsOfFun TonsOfFun commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Positioning update (latest commit): the core framework stays free, but production observability/monitoring is the hosted platform's paid product — free workspaces get a low-volume trial. The engine is now documented as a local dev console: README, CHANGELOG, and docs/framework/dashboard.md reframed accordingly (the parity table states intended use: console = development, platform = production). All engine fixes below are unchanged and still needed — they make the dev/trial experience work.

Why

The dashboard engine that shipped in 1.0.3 is the local dev console counterpart to the activeagents.ai cloud platform, but it was undocumented and not actually usable in a host app: its classes never landed on autoload paths, local_storage telemetry silently persisted nothing, most engine routes pointed at controllers that don't exist, token totals were exactly doubled, and every dashboard page 500'd in apps without turbo-rails. The activeagents.ai platform (which runs this engine multi-tenant — see activeagents/activeagents#90) needed manual require workarounds and app-side patches for all of this.

This PR makes the engine work out of the box for v2 and the cloud launch. All fixes were verified two ways: a new engine test suite against the gem's dummy app, and the full activeagents.ai platform test suite (166 runs) run against this branch with every workaround removed.

What

Engine boot (the big one)

  • Engine.find_root now returns the dashboard directory, and is defined before isolate_namespace (which materializes config via routes, freezing the root). Previously only Engine.root was overridden, which Rails never consults for load paths — so ActiveAgent::TelemetryTrace, ProcessTelemetryTracesJob, the ingest controller, views, and the engine's routes file were all invisible to host apps.
  • The engine is required eagerly when Rails is present (lib/active_agent.rb). It was previously behind autoload :Dashboard, and an engine defined after the host app collects railtie initializers never registers its paths.

Routes / controllers

  • Engine routes trimmed to what actually ships: traces, metrics, and the ingest API. The removed routes (agents CRUD, runs, templates, sandboxes, recordings, api/v1/*) pointed at controllers that don't exist in the gem and produced 500s; the models remain for host apps that use them. A route↔controller consistency test now guards this.
  • Ingest is POST /api/traces, matching Telemetry::Configuration::LOCAL_ENDPOINT_PATH (the routes previously declared api/v1 while the controller lives at Api::TracesController and the config pointed at api/traces — three different answers).
  • Engine root renders the traces index; dashboard#index redirects in ERB mode instead of rendering a view that doesn't exist (and no longer queries Agent/AgentRun tables that telemetry-only installs don't have). Added the Agent.for_owner scope DashboardController referenced but nothing defined.

Telemetry correctness

  • local_storage mode: Tracer#build_trace_payload emits symbol keys, create_from_payload reads string keys — every locally stored trace failed trace_id presence validation inside a rescued block, i.e. the flagship local mode persisted nothing. The reporter now deep-stringifies and honors ActiveAgent::Dashboard.trace_model overrides instead of hardcoding the class.
  • Token totals: instrumentation mirrors LLM usage onto both the llm span and root span; create_from_payload summed all spans, so every trace reported exactly 2× tokens. Child spans are now the source of truth when they carry token data (root-only traces still count the root).
  • Synchronous ingest capped at 100 traces/request, mirroring ProcessTelemetryTracesJob::MAX_TRACES_PER_JOB.

Views

  • Span waterfall computes relative offsets from span start_time (the template read start_time_relative_ms, which no emitter produces — every bar rendered at 0ms). Also fixes a clamp crash for spans starting at 100%.
  • turbo_stream responses are gated on the MIME type being registered — turbo-rails is not a dependency, and without it every dashboard page raised.
  • Layout used an undefined active_agent_dashboard. route proxy (the mount helper is active_agent); engine views now use engine-local helpers.

Docs & tests

  • docs/framework/dashboard.md: install, what you get, authentication (none by default — stated loudly), remote ingestion, multi-tenant mode, and the dev-console ↔ cloud relationship table. README gains a Dev Console & Observability section. CHANGELOG entry under Unreleased.
  • New test/dashboard/ suite (16 tests): payload normalization/dedup, engine paths + autoload, route↔controller consistency, page rendering, LOCAL_ENDPOINT_PATH ingest round-trip, and symbol-keyed reporter persistence — run against the dummy app (now mounts the engine).

Verification

  • test/dashboard/: 16 runs, 0 failures/errors (rails8 gemfile)
  • Full gem suite: identical results to unmodified main apart from the new tests passing (the pre-existing errors in both runs are missing OPENAI_API_KEY-style credential errors in this environment, not regressions)
  • activeagents.ai platform suite (166 runs, 536 assertions) green against this branch with its engine-require workarounds deleted — engine classes autoload, ingest/dedup/read APIs all work through the gem

Notes for reviewers

🤖 Generated with Claude Code

https://claude.ai/code/session_014h5Yw7jE62UyPPAF1h7Mr5


Generated by Claude Code

TonsOfFun and others added 3 commits July 15, 2026 01:35
The dashboard engine shipped in 1.0.3 was not usable in host apps without
manual workarounds. This makes it work out of the box:

- Engine.find_root now returns the dashboard directory (defined before
  isolate_namespace materializes config), so app/{models,controllers,jobs,
  views} and config/routes.rb land on host apps' load paths; the engine is
  required eagerly with Rails since lazily-autoloaded engines miss
  initializer collection
- Routes trimmed to shipped controllers (traces, metrics, api/traces
  ingest matching LOCAL_ENDPOINT_PATH); removed routes to never-shipped
  controllers that 500'd; engine root renders traces#index and
  dashboard#index redirects in ERB mode instead of rendering a missing view
- Reporter local_storage mode: stringify symbol-keyed tracer payloads
  (previously every locally-stored trace failed validation silently) and
  honor ActiveAgent::Dashboard.trace_model overrides
- create_from_payload: count child spans as token source of truth —
  instrumentation mirrors llm tokens onto the root span and totals were
  exactly doubled
- Trace detail waterfall: compute relative span offsets from start_time
  (start_time_relative_ms was never emitted; all bars rendered at 0), fix
  clamp crash when a span starts at 100%
- turbo_stream responses gated on the MIME type being registered
  (turbo-rails is optional; without it every dashboard page raised)
- Layout used an undefined active_agent_dashboard route proxy; engine
  views use engine-local helpers
- Add Agent.for_owner scope (referenced by DashboardController, never
  defined); cap synchronous ingest at 100 traces/request like the async job
- Docs: docs/framework/dashboard.md self-hosting guide (auth is nil by
  default — documented loudly), README section, CHANGELOG; dashboard
  engine test suite (test/dashboard/) run against the dummy app

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014h5Yw7jE62UyPPAF1h7Mr5
- Fail closed in production: the dashboard now returns 403 with setup
  instructions when authentication_method is unconfigured, instead of
  serving prompts/outputs/backtraces to the internet by default
- ProcessTelemetryTracesJob re-enqueues traces beyond the 100-per-job cap
  instead of silently dropping them after the client got 202
- Remove for_owner scope added earlier on Dashboard::Agent — the shared
  ApplicationRecord base already defines it (more defensively); the new
  scope shadowed it
- Installer post-install message no longer references the nonexistent
  active_agent:dashboard:seed task
- Mark capture_bodies/redact_attributes as reserved (documented but not
  yet consumed) so the config surface is honest
- CHANGELOG: place Unreleased under the Keep-a-Changelog preamble

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014h5Yw7jE62UyPPAF1h7Mr5
Product decision: the core framework stays free, but production
observability and monitoring are the hosted platform's paid product
(free workspaces get a low-volume trial). The engine keeps working
exactly as before — this reframes README, CHANGELOG, and
docs/framework/dashboard.md from "free self-hosted production
observability" to "dev console while you build, platform for
production", and the parity table now states the intended use of each.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014h5Yw7jE62UyPPAF1h7Mr5
@TonsOfFun TonsOfFun changed the title Make the self-hosted dashboard engine production-ready Make the dashboard engine (local dev console) work out of the box Jul 15, 2026
…enerators

Three gaps surfaced by building an example app against this branch:

- Instrumented traces couldn't be correlated with anything: the tracer
  always generated its own trace_id, while the generation parameters (and
  e.g. solid_agent's persisted generations) carry
  prompt_options[:trace_id]. Instrumentation now mints/reuses
  prompt_options[:trace_id] and passes it to Telemetry.trace; the tracer
  honors a caller-supplied trace_id and generates one only as fallback.
- Reporter#flush was fire-and-forget: the batch went to a detached thread
  nothing joined, so short-lived processes (rails runner, jobs, rolling
  restarts) silently dropped traces. flush now waits for the send to
  complete, shutdown joins in-flight send threads, and the railtie
  installs an at_exit shutdown when telemetry is enabled.
- Dashboard install generator migrations used t.jsonb, which raises on
  SQLite — Rails' default database and the primary dev-console audience.
  Templates now use t.json (works on both SQLite and Postgres).

New test/dashboard/telemetry_correlation_test.rb covers: caller-supplied
trace ids, generated fallback ids, deterministic flush (no sleeps),
shutdown flushing the buffer, and an instrumented generation storing its
trace under the prompt_options UUID (dashed-UUID format proves the id
flowed from prompt_options rather than the tracer's hex generator).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014h5Yw7jE62UyPPAF1h7Mr5
@TonsOfFun
TonsOfFun merged commit 2644027 into main Jul 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants