Current section
Files
Jump to
Current section
Files
lib/mix/tasks/ragex.dashboard.ex
defmodule Mix.Tasks.Ragex.Dashboard do
@moduledoc """
Live monitoring dashboard for Ragex metrics.
## Usage
# Launch dashboard
mix ragex.dashboard
# With custom refresh interval
mix ragex.dashboard --interval 2000
Displays real-time statistics in a TUI:
- Graph statistics (nodes, edges, modules, functions)
- Embedding metrics (count, dimensions, cache status)
- Cache performance (hit rates, size)
- AI usage tracking (requests, tokens, costs)
- Recent activity log
Press 'q' or Ctrl+C to exit.
"""
@shortdoc "Live monitoring dashboard"
use Mix.Task
alias Ragex.AI.{Cache, Usage}
alias Ragex.CLI.Colors
alias Ragex.Embeddings.Persistence
alias Ragex.Graph.Store
@refresh_interval 1000
@impl Mix.Task
def run(args) do
{:ok, _} = Application.ensure_all_started(:ragex)
{opts, _, _} =
OptionParser.parse(args,
switches: [interval: :integer, help: :boolean],
aliases: [i: :interval, h: :help]
)
if opts[:help] do
show_help()
else
interval = opts[:interval] || @refresh_interval
run_dashboard(interval)
end
end
defp run_dashboard(interval) do
IO.puts(Colors.bold("\n" <> "Ragex Dashboard"))
IO.puts(Colors.muted("Press Ctrl+C to exit\n"))
# Initial render
render_dashboard()
# Start refresh loop
refresh_loop(interval)
end
defp refresh_loop(interval) do
Process.sleep(interval)
# Clear screen and move to top
IO.write(IO.ANSI.clear() <> IO.ANSI.home())
render_dashboard()
refresh_loop(interval)
end
defp render_dashboard do
timestamp = DateTime.utc_now() |> DateTime.to_string()
IO.puts(Colors.bold("Ragex Dashboard") <> Colors.muted(" - #{timestamp}"))
IO.puts(String.duplicate("=", 80))
IO.puts("")
# Row 1: Graph & Embeddings
graph_stats = fetch_graph_stats()
embedding_stats = fetch_embedding_stats()
render_side_by_side(
render_graph_panel(graph_stats),
render_embeddings_panel(embedding_stats)
)
IO.puts("")
# Row 2: Cache & AI Usage
cache_stats = fetch_cache_stats()
ai_stats = fetch_ai_stats()
render_side_by_side(
render_cache_panel(cache_stats),
render_ai_panel(ai_stats)
)
IO.puts("")
# Bottom: Activity log
render_activity_panel()
IO.puts("")
IO.puts(Colors.muted("Press Ctrl+C to exit"))
end
defp render_side_by_side(left_lines, right_lines) do
# Render two panels side by side
max_lines = max(length(left_lines), length(right_lines))
left_width = 38
for i <- 0..(max_lines - 1) do
left_line = Enum.at(left_lines, i, "")
right_line = Enum.at(right_lines, i, "")
# Pad left line to width
left_padded = String.pad_trailing(left_line, left_width)
IO.puts(left_padded <> " " <> right_line)
end
end
defp render_graph_panel(stats) do
[
Colors.bold("┌─ Graph Statistics ─────────────────┐"),
"│ " <> format_stat_line("Modules", stats.modules),
"│ " <> format_stat_line("Functions", stats.functions),
"│ " <> format_stat_line("Total Nodes", stats.total_nodes),
"│ " <> format_stat_line("Total Edges", stats.total_edges),
"│ " <> format_stat_line("Avg Degree", stats.avg_degree),
Colors.bold("└────────────────────────────────────┘")
]
end
defp render_embeddings_panel(stats) do
status_color =
if stats.count > 0 do
Colors.success("Active")
else
Colors.muted("Empty")
end
[
Colors.bold("┌─ Embeddings ───────────────────────┐"),
"│ " <> format_stat_line("Status", status_color),
"│ " <> format_stat_line("Count", stats.count),
"│ " <> format_stat_line("Model", stats.model),
"│ " <> format_stat_line("Dimensions", stats.dimensions),
"│ " <> format_stat_line("Cache Size", stats.cache_size),
Colors.bold("└────────────────────────────────────┘")
]
end
defp render_cache_panel(stats) do
hit_rate = if stats.total > 0, do: Float.round(stats.hits / stats.total * 100, 1), else: 0.0
hit_rate_color =
cond do
hit_rate >= 80 -> Colors.success("#{hit_rate}%")
hit_rate >= 50 -> Colors.warning("#{hit_rate}%")
true -> Colors.error("#{hit_rate}%")
end
[
Colors.bold("┌─ Cache Performance ────────────────┐"),
"│ " <> format_stat_line("Hit Rate", hit_rate_color),
"│ " <> format_stat_line("Hits", Colors.success(to_string(stats.hits))),
"│ " <> format_stat_line("Misses", Colors.error(to_string(stats.misses))),
"│ " <> format_stat_line("Total Entries", stats.size),
"│ " <> format_stat_line("Evictions", stats.evictions),
Colors.bold("└────────────────────────────────────┘")
]
end
defp render_ai_panel(stats) do
cost_display =
if stats.total_cost > 0 do
Colors.warning("$#{Float.round(stats.total_cost, 2)}")
else
Colors.muted("$0.00")
end
[
Colors.bold("┌─ AI Usage ─────────────────────────┐"),
"│ " <> format_stat_line("Requests", stats.requests),
"│ " <> format_stat_line("Total Tokens", format_large_number(stats.tokens)),
"│ " <> format_stat_line("Est. Cost", cost_display),
"│ " <> format_stat_line("Active Providers", stats.active_providers),
"│ " <> format_stat_line("Cache Hits", stats.cache_hits),
Colors.bold("└────────────────────────────────────┘")
]
end
defp render_activity_panel do
IO.puts(
Colors.bold(
"┌─ Recent Activity ──────────────────────────────────────────────────────────┐"
)
)
# [TODO] Get recent activity (placeholder - would need actual activity tracking)
activities = []
# [
# {DateTime.utc_now() |> DateTime.add(-10, :second), "Analyzed 15 files in lib/"},
# {DateTime.utc_now() |> DateTime.add(-45, :second), "Generated 43 embeddings"},
# {DateTime.utc_now() |> DateTime.add(-120, :second), "Refactored MyModule.process/2"}
# ]
if activities == [] do
IO.puts("│ " <> Colors.muted("No recent activity"))
else
for {time, message} <- Enum.take(activities, 5) do
time_ago = format_time_ago(time)
IO.puts("│ #{Colors.muted(time_ago)} #{message}")
end
end
IO.puts(
Colors.bold(
"└────────────────────────────────────────────────────────────────────────────┘"
)
)
end
defp format_stat_line(label, value) do
# Format: "Label........: Value" with proper padding
label_padded = String.pad_trailing(label, 16)
value_str = to_string(value) |> String.slice(0, 18)
"#{label_padded}: #{value_str}" |> String.pad_trailing(34)
end
defp format_large_number(num) when num >= 1_000_000 do
"#{Float.round(num / 1_000_000, 1)}M"
end
defp format_large_number(num) when num >= 1_000 do
"#{Float.round(num / 1_000, 1)}K"
end
defp format_large_number(num), do: to_string(num)
defp format_time_ago(time) do
diff = DateTime.diff(DateTime.utc_now(), time, :second)
cond do
diff < 60 -> "#{diff}s ago"
diff < 3_600 -> "#{div(diff, 60)}m ago"
diff < 86_400 -> "#{div(diff, 3_600)}h ago"
true -> "#{div(diff, 86400)}d ago"
end
|> String.pad_trailing(10)
end
defp fetch_graph_stats do
modules = Store.list_nodes(:module)
functions = Store.list_nodes(:function)
all_nodes = Store.list_nodes()
edges = Store.list_edges()
avg_degree =
case all_nodes do
[_ | _] -> Float.round(length(edges) * 2 / length(all_nodes), 1)
_ -> 0.0
end
%{
modules: length(modules),
functions: length(functions),
total_nodes: length(all_nodes),
total_edges: length(edges),
avg_degree: avg_degree
}
end
defp fetch_embedding_stats do
embeddings = Store.list_embeddings()
count = length(embeddings)
{model, dimensions} =
if count > 0 do
{_type, _id, embedding, _text} = hd(embeddings)
model_id = Application.get_env(:ragex, :embedding_model, :all_minilm_l6_v2)
{model_id, length(embedding)}
else
{:none, 0}
end
cache_size =
case Persistence.cache_stats() do
{:ok, stats} -> format_bytes(stats.file_size)
_ -> "N/A"
end
%{
count: count,
model: model,
dimensions: dimensions,
cache_size: cache_size
}
end
defp fetch_cache_stats do
stats = Cache.stats()
%{
hits: stats.hits,
misses: stats.misses,
total: stats.hits + stats.misses,
size: stats.size,
evictions: stats.evictions
}
end
defp fetch_ai_stats do
all_stats = Usage.get_stats(:all)
{requests, tokens, cost, providers} =
Enum.reduce(all_stats, {0, 0, 0.0, 0}, fn {_provider, provider_stats},
{req, tok, cost_acc, prov} ->
{
req + provider_stats.total_requests,
tok + provider_stats.total_tokens,
cost_acc + provider_stats.estimated_cost,
prov + 1
}
end)
cache_stats = Cache.stats()
%{
requests: requests,
tokens: tokens,
total_cost: cost,
active_providers: providers,
cache_hits: cache_stats.hits
}
end
defp format_bytes(bytes) when bytes >= 1_073_741_824 do
"#{Float.round(bytes / 1_073_741_824, 1)} GB"
end
defp format_bytes(bytes) when bytes >= 1_048_576 do
"#{Float.round(bytes / 1_048_576, 1)} MB"
end
defp format_bytes(bytes) when bytes >= 1024 do
"#{Float.round(bytes / 1024, 1)} KB"
end
defp format_bytes(bytes), do: "#{bytes} B"
defp show_help do
IO.puts("""
#{Colors.bold("Ragex Dashboard")}
#{Colors.info("Launch dashboard:")}
mix ragex.dashboard
#{Colors.info("Custom refresh interval (milliseconds):")}
mix ragex.dashboard --interval 2000
#{Colors.info("What's displayed:")}
• Graph statistics (nodes, edges, modules, functions)
• Embedding metrics (count, model, dimensions, cache)
• Cache performance (hit rate, evictions)
• AI usage (requests, tokens, costs, providers)
• Recent activity log
#{Colors.muted("Press Ctrl+C to exit the dashboard.")}
""")
end
end