#!/usr/bin/env python3
"""Costs Tab — Daily cost stats + 7-day bar chart (Cairo) + tool rankings."""
import math
import sys
from pathlib import Path
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
sys.path.insert(0, str(Path.home() / "Projekte" / "ClaudeCodePanel"))
from theme import get_palette
from monitor import get_daily_cost, get_usage_timeline, get_provider_costs, get_top_tools, format_cost
class CostsTab(Gtk.Box):
"""Cost overview with stats, bar chart, and tool rankings."""
def __init__(self):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
self.set_margin_top(12)
self.set_margin_start(12)
self.set_margin_end(12)
self._stat_labels: dict[str, Gtk.Label] = {}
self._timeline_data: list[dict] = []
self._chart_area = None
self._tools_box = None
self._providers_box = None
self._build_ui()
def _build_ui(self):
p = get_palette()
# --- Stats row ---
stats_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=20)
stats_box.get_style_context().add_class("base-card")
for key, label in [("today", "Today"), ("calls", "Calls"), ("tools", "Tools")]:
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
val_lbl = Gtk.Label(label="--")
val_lbl.get_style_context().add_class("stat-value" if key == "today" else "monitor-value")
vbox.pack_start(val_lbl, False, False, 0)
name_lbl = Gtk.Label(label=label)
name_lbl.get_style_context().add_class("stat-label")
vbox.pack_start(name_lbl, False, False, 0)
stats_box.pack_start(vbox, True, True, 0)
self._stat_labels[key] = val_lbl
self.pack_start(stats_box, False, False, 0)
# --- Bar chart (7 days) ---
chart_label = Gtk.Label()
chart_label.set_markup('Last 7 Days')
chart_label.set_halign(Gtk.Align.START)
chart_label.get_style_context().add_class("section-title")
self.pack_start(chart_label, False, False, 0)
self._chart_area = Gtk.DrawingArea()
self._chart_area.set_size_request(-1, 120)
self._chart_area.connect("draw", self._draw_chart)
self.pack_start(self._chart_area, False, False, 0)
# --- Provider costs ---
prov_label = Gtk.Label()
prov_label.set_markup('Provider Costs')
prov_label.set_halign(Gtk.Align.START)
prov_label.get_style_context().add_class("section-title")
self.pack_start(prov_label, False, False, 0)
self._providers_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
self._providers_box.get_style_context().add_class("base-card")
self.pack_start(self._providers_box, False, False, 0)
# --- Top tools ---
tools_label = Gtk.Label()
tools_label.set_markup('Top Tools')
tools_label.set_halign(Gtk.Align.START)
tools_label.get_style_context().add_class("section-title")
self.pack_start(tools_label, False, False, 0)
self._tools_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
self._tools_box.get_style_context().add_class("base-card")
self.pack_start(self._tools_box, False, False, 0)
def refresh(self) -> None:
"""Reload cost data from monitor.py."""
p = get_palette()
# Daily cost
daily = get_daily_cost()
cost = daily.get("cost_estimate_usd", 0)
self._stat_labels["today"].set_text(format_cost(cost))
self._stat_labels["calls"].set_text(str(daily.get("total_calls", 0)))
self._stat_labels["tools"].set_text(str(daily.get("unique_tools", 0)))
# Timeline for chart
self._timeline_data = get_usage_timeline()
self._chart_area.queue_draw()
# Provider costs
for child in self._providers_box.get_children():
self._providers_box.remove(child)
providers = get_provider_costs()
if providers:
for prov, cost_val in sorted(providers.items(), key=lambda x: -x[1]):
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
name_lbl = Gtk.Label()
name_lbl.set_markup(f'{GLib.markup_escape_text(prov)}')
name_lbl.set_xalign(0)
name_lbl.set_width_chars(15)
row.pack_start(name_lbl, False, False, 0)
cost_lbl = Gtk.Label()
cost_lbl.set_markup(
f'{format_cost(cost_val)}'
)
cost_lbl.set_halign(Gtk.Align.END)
row.pack_end(cost_lbl, False, False, 0)
self._providers_box.pack_start(row, False, False, 0)
else:
lbl = Gtk.Label()
lbl.set_markup(f'No cost data yet')
self._providers_box.pack_start(lbl, False, False, 0)
self._providers_box.show_all()
# Top tools
for child in self._tools_box.get_children():
self._tools_box.remove(child)
top = get_top_tools(n=8)
if top:
for tool_name, count in top:
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
name_lbl = Gtk.Label()
name_lbl.set_markup(f'{GLib.markup_escape_text(tool_name)}')
name_lbl.set_xalign(0)
name_lbl.set_width_chars(20)
row.pack_start(name_lbl, False, False, 0)
count_lbl = Gtk.Label()
count_lbl.set_markup(
f'{count}'
)
row.pack_end(count_lbl, False, False, 0)
self._tools_box.pack_start(row, False, False, 0)
else:
lbl = Gtk.Label()
lbl.set_markup(f'No tool data yet')
self._tools_box.pack_start(lbl, False, False, 0)
self._tools_box.show_all()
def _draw_chart(self, widget, cr):
"""Draw 7-day bar chart using Cairo."""
p = get_palette()
width = widget.get_allocated_width()
height = widget.get_allocated_height()
data = list(reversed(self._timeline_data)) # oldest first for left-to-right
if not data:
# Draw placeholder
cr.set_source_rgba(*_hex_rgb(p["dim"]), 0.5)
cr.select_font_face("monospace", 0, 0)
cr.set_font_size(11)
cr.move_to(width / 2 - 40, height / 2)
cr.show_text("No data yet")
return False
n = len(data)
max_cost = max((d.get("cost_est", 0) for d in data), default=1) or 0.01
bar_width = max(20, (width - 40) / n - 8)
gap = 8
x_start = 20
chart_height = height - 30 # leave room for labels
for i, d in enumerate(data):
cost = d.get("cost_est", 0)
bar_h = max(2, (cost / max_cost) * (chart_height - 10))
x = x_start + i * (bar_width + gap)
y = chart_height - bar_h
# Bar
r, g, b = _hex_rgb(p["accent"])
cr.set_source_rgba(r, g, b, 0.7)
cr.rectangle(x, y, bar_width, bar_h)
cr.fill()
# Date label
cr.set_source_rgba(*_hex_rgb(p["dim"]), 1)
cr.select_font_face("monospace", 0, 0)
cr.set_font_size(9)
date_str = d.get("date", "")[-5:] # MM-DD
cr.move_to(x, height - 4)
cr.show_text(date_str)
# Cost label on top of bar
cr.set_source_rgba(*_hex_rgb(p["text"]), 1)
cr.set_font_size(9)
cr.move_to(x, y - 3)
cr.show_text(format_cost(cost))
return False
def _hex_rgb(hex_color: str) -> tuple[float, float, float]:
"""Convert #rrggbb to (r, g, b) floats 0-1."""
return (
int(hex_color[1:3], 16) / 255,
int(hex_color[3:5], 16) / 255,
int(hex_color[5:7], 16) / 255,
)