"""Overview tab — the card-based dashboard (Token Usage & Cost, Resource Usage, Recent Activity, Sandbox Details + nested Permissions, Model Pricing, Audit Log preview). Extracted from ``ui/monitoring_tab.py``'s ``_build_overview_page`` and the refresh/pricing/budget methods it wires to. """ from __future__ import annotations import time from typing import Callable, List from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QGridLayout, QGroupBox, QHBoxLayout, QLabel, QProgressBar, QPushButton, QScrollArea, QVBoxLayout, QWidget, ) from ....core import usage_tracker as ut from ....i18n import tr from ....theme import current_palette from ....ui.icons import DOT_AMBER, DOT_GREEN, DOT_RED, icon from ....ui.widgets import BudgetCard, StatCard, fmt_tokens from ..shared.formatters import fmt_bytes, relative_time from .pricing_panel import PricingPanel from .sandbox_tab import SandboxDetailsCard class OverviewTab(QWidget): def __init__(self, ctx, *, on_status_message: Callable[[str], None], on_settings_changed: Callable[[], None], on_view_all_action_logs: Callable[[], None], action_logs_tab_visible: bool): super().__init__() self.ctx = ctx self._on_status_message = on_status_message self._on_view_all_action_logs = on_view_all_action_logs self._last_io_sample = None self._res_first = True outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QScrollArea.NoFrame) content = QWidget() scroll.setWidget(content) outer.addWidget(scroll) # ONE main column, scrolled vertically, sections in a fixed order. root = QVBoxLayout(content) root.setSpacing(12) self._build_usage_section(root) self._build_activity_section() # added to `root` further down, beside the audit log self._build_resource_section(root) self.sandbox_card = SandboxDetailsCard(ctx, on_settings_changed) root.addWidget(self.sandbox_card) self.pricing_panel = PricingPanel(ctx, on_status_message) root.addWidget(self.pricing_panel) root.addWidget(self.activity_group) self._build_audit_section(root, action_logs_tab_visible) root.addStretch(1) # ---- Token Usage & Cost ------------------------------------------------ def _build_usage_section(self, root: QVBoxLayout) -> None: self.usage_group = QGroupBox() self.usage_group.setObjectName("monSection") usage_lay = QGridLayout(self.usage_group) usage_lay.setSpacing(8) self.usage_total = StatCard() self.usage_in = StatCard() self.usage_out = StatCard() self.usage_cache = StatCard() self.usage_cost = StatCard() self.usage_calls = StatCard() for i, card in enumerate((self.usage_cost, self.usage_total, self.usage_in, self.usage_out, self.usage_cache)): usage_lay.addWidget(card, 0, i) self.usage_calls.setVisible(False) # rides on the cost tile's label self.budget_card = BudgetCard() self.budget_card.apply_btn.setIcon(icon("check")) self.budget_card.apply_btn.clicked.connect(self._apply_budget) usage_lay.addWidget(self.budget_card, 0, 3) for col in range(4): usage_lay.setColumnStretch(col, 1) root.addWidget(self.usage_group) # ---- Recent Activity ---------------------------------------------------- def _build_activity_section(self) -> None: self.activity_group = QGroupBox() self.activity_group.setObjectName("monSection") act_lay = QVBoxLayout(self.activity_group) self.activity_lbl = QLabel() self.activity_lbl.setWordWrap(True) self.activity_lbl.setTextFormat(Qt.RichText) act_lay.addWidget(self.activity_lbl) # ---- Resource Usage ------------------------------------------------------- def _build_resource_section(self, root: QVBoxLayout) -> None: self.resource_group = QGroupBox() self.resource_group.setObjectName("monSection") res_lay = QHBoxLayout(self.resource_group) res_lay.setSpacing(6) def _pair(): if not self._res_first: sep = QLabel(chr(0xB7)) sep.setObjectName("hint") res_lay.addWidget(sep) self._res_first = False lbl = QLabel() lbl.setObjectName("hint") val = QLabel() res_lay.addWidget(lbl) res_lay.addWidget(val) return lbl, val def _bar_row(): lbl, val = _pair() bar = QProgressBar() bar.setVisible(False) return lbl, bar, val self.cpu_lbl, self.cpu_bar, self.cpu_val = _bar_row() self.mem_lbl, self.mem_bar, self.mem_val = _bar_row() self.diskfree_lbl, self.diskfree_val = _pair() self.disk_lbl, self.disk_val = QLabel(), QLabel() self.network_lbl, self.network_val = QLabel(), QLabel() res_lay.addStretch(1) root.addWidget(self.resource_group) # ---- Audit Log preview -------------------------------------------------- def _build_audit_section(self, root: QVBoxLayout, action_logs_tab_visible: bool) -> None: self.audit_group = QGroupBox() self.audit_group.setObjectName("monSection") audit_lay = QVBoxLayout(self.audit_group) self.audit_lbl = QLabel() self.audit_lbl.setWordWrap(True) self.audit_lbl.setTextFormat(Qt.RichText) audit_lay.addWidget(self.audit_lbl) self.view_all_btn = QPushButton() self.view_all_btn.setFlat(True) self.view_all_btn.clicked.connect(lambda: self._on_view_all_action_logs()) audit_lay.addWidget(self.view_all_btn, 0, Qt.AlignRight) self.audit_group.setVisible(action_logs_tab_visible) root.addWidget(self.audit_group) # ---- budget -------------------------------------------------------------- def _apply_budget(self) -> None: """Persist the spin box's value as the new budget — starts a fresh remaining-balance window (spend before now is no longer counted).""" ccy = (self.ctx.config.data.get("usage") or {}).get("currency", "USD") ut.set_budget(self.ctx.config, self.budget_card.budget_spin.value(), ccy) self.ctx.save() self._refresh_budget() def _refresh_budget(self) -> None: from ....core import model_pricing as mp pricing = {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})} status = ut.budget_status(self.ctx.config) if status is None: self.budget_card.set(tr("usage.budget_title"), "—", tr("usage.budget_no_budget")) self.budget_card.budget_spin.setValue(0.0) return amount_disp = mp.convert(status["amount_usd"], "USD", pricing.get("currency", "USD"), self.ctx.config) value = (f"{ut.format_cost(status['remaining_usd'], pricing, digits=2)}" f" / {ut.format_cost(status['amount_usd'], pricing, digits=2)}") pct = int(round(status["pct_used"] * 100)) sub = tr("usage.budget_over_warning") if status["over_85"] else tr("usage.budget_used_pct", pct=pct) self.budget_card.set(tr("usage.budget_title"), value, sub, warn=status["over_85"]) if not self.budget_card.budget_spin.hasFocus(): self.budget_card.budget_spin.setValue(round(amount_disp, 2)) # ---- resource usage -------------------------------------------------------- def _refresh_resource_usage(self) -> None: try: import psutil except ImportError: self._set_resource_na() return try: own = psutil.Process() own_cpu = own.cpu_percent(interval=None) own_mem = own.memory_info().rss except Exception: own, own_cpu, own_mem = None, 0.0, 0 self.cpu_bar.setValue(int(min(own_cpu, 100))) self.cpu_val.setText(f"{own_cpu:.0f}%") try: total_mem = psutil.virtual_memory().total mem_pct = int(own_mem * 100 / total_mem) if total_mem else 0 except Exception: mem_pct = 0 self.mem_bar.setValue(min(mem_pct, 100)) try: self.mem_val.setText(f"{fmt_bytes(own_mem)}/{fmt_bytes(total_mem)}") except Exception: # noqa: BLE001 self.mem_val.setText(fmt_bytes(own_mem)) try: free = psutil.disk_usage(str(self.ctx.config.cowork_output_dir())).free self.diskfree_val.setText(tr("monitoring.overview_disk_free", size=fmt_bytes(free))) except Exception: # noqa: BLE001 self.diskfree_val.setText(tr("monitoring.na")) now = time.monotonic() try: io = own.io_counters() if own is not None else None disk_bytes = (io.read_bytes + io.write_bytes) if io is not None else None except Exception: disk_bytes = None try: net = psutil.net_io_counters() net_bytes = net.bytes_sent + net.bytes_recv except Exception: net_bytes = None prev = self._last_io_sample self._last_io_sample = (now, disk_bytes, net_bytes) na = tr("monitoring.na") if prev and disk_bytes is not None and prev[1] is not None and now > prev[0]: rate = max(0.0, (disk_bytes - prev[1]) / (now - prev[0])) self.disk_val.setText(f"{fmt_bytes(rate)}/s") else: self.disk_val.setText(na) if prev and net_bytes is not None and prev[2] is not None and now > prev[0]: rate = max(0.0, (net_bytes - prev[2]) / (now - prev[0])) self.network_val.setText(f"{fmt_bytes(rate)}/s") else: self.network_val.setText(na) def _set_resource_na(self) -> None: na = tr("monitoring.na") self.cpu_bar.setValue(0) self.cpu_val.setText(na) self.mem_bar.setValue(0) self.mem_val.setText(na) self.disk_val.setText(na) self.network_val.setText(na) # ---- usage cards ----------------------------------------------------------- def _activity_line(self, event: dict) -> str: ok = event.get("ok", True) if ok: mark = f"✓" elif event.get("kind") == "security_block": mark = f"!" else: mark = f"✗" name = event.get("name", "") or event.get("kind", "") rel = relative_time(event.get("ts", "")) muted = current_palette().text_muted suffix = f" — {rel}" if rel else "" return f"{mark} {name}{suffix}" def _refresh_usage_cards(self) -> None: from ....core import model_pricing as mp mp.sync_to_usage(self.ctx.config) # cost total comes straight from the price table pricing = {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})} events = ut.load_events() s = ut.summarize(events) costs = ut.cost_usd_events(events, pricing) self.usage_total.set(tr("dashboard.card_total"), fmt_tokens(s["total"])) self.usage_calls.set(tr("monitoring.overview_calls"), str(s["turns"]), "") self.usage_in.set(tr("dashboard.card_in"), fmt_tokens(s["in"]), ut.format_cost(costs["in"], pricing)) self.usage_out.set(tr("dashboard.card_out"), fmt_tokens(s["out"]), ut.format_cost(costs["out"], pricing)) self.usage_cache.set(tr("dashboard.card_cache"), fmt_tokens(s["cache"]), ut.format_cost(costs["cache"], pricing)) self.usage_cost.set( f'{tr("dashboard.card_cost")} · {s["turns"]} {tr("monitoring.overview_calls")}', ut.format_cost(sum(costs.values()), pricing, digits=2)) self._refresh_budget() # ---- public API used by the container --------------------------------- def refresh(self, events: List[dict]) -> None: """Full refresh — resource usage + usage cards + sandbox/permissions + recent activity + audit preview. ``events`` is the already-loaded (local-or-shared) audit log, shared with the event-table tabs so the decision of which source to read from is made exactly once per refresh tick.""" self._refresh_resource_usage() self._refresh_usage_cards() self.sandbox_card.refresh() recent = sorted(events, key=lambda e: e.get("ts", ""), reverse=True) if recent: self.activity_lbl.setText("
".join(self._activity_line(e) for e in recent[:6])) self.audit_lbl.setText("
".join( f"{e.get('ts', '')} — {e.get('name', '')}" for e in recent[:4])) else: self.activity_lbl.setText(tr("monitoring.overview_no_activity")) self.audit_lbl.setText(tr("monitoring.overview_no_activity")) def retranslate(self) -> None: self.usage_group.setTitle(tr("monitoring.overview_usage_title").upper().replace("&", "&&")) self.budget_card.apply_btn.setToolTip(tr("usage.budget_apply_tooltip")) self.budget_card.budget_spin.setToolTip(tr("usage.budget_spin_tooltip")) self.activity_group.setTitle(tr("monitoring.overview_activity_title").upper()) self.resource_group.setTitle(tr("monitoring.overview_resource_title").upper()) self.cpu_lbl.setText(tr("monitoring.overview_res_cpu")) self.mem_lbl.setText(tr("monitoring.overview_res_mem")) self.diskfree_lbl.setText(tr("monitoring.overview_disk_label")) self.disk_lbl.setText(tr("monitoring.overview_res_disk")) self.network_lbl.setText(tr("monitoring.overview_res_network")) self.pricing_panel.retranslate() self.sandbox_card.retranslate() self.audit_group.setTitle(tr("monitoring.overview_audit_title").upper()) self.view_all_btn.setText(tr("monitoring.overview_view_all"))