- ui/monitoring_tab.py (1546 dong) tach thanh presentation/monitoring/** (container + 7 tab/card + shared helper), ui/monitoring_tab.py con lai re-export shim de app.py khong doi. - infrastructure/telemetry/audit_logger.py: CanonicalAuditLogger, core/audit_log.py thanh wrapper mong, tuong thich nguoc 100% voi schema .jsonl cu. - application/monitoring/monitoring_query_service.py: MonitoringQueryService read-only, filter/sort/pagination, khong import PySide6. - Go circular import model_pricing<->usage_tracker va agent_security<-> agent_security_alert (core/agent_security_types.py moi). - infrastructure/sandbox/sandbox_capabilities.py: SandboxCapabilityMatrix theo OS (Windows/Linux/macOS), chua dau noi vao core/sandbox_manager.py. - conftest.py: sua loi checkout khong ten cowork_local khien pytest import nham thu muc khac. - 77 test moi, 167/167 pass. QA da xac nhan UI/business logic khong doi (xem evidence/report/unified_report.html). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
213 lines
9.1 KiB
Python
213 lines
9.1 KiB
Python
"""Monitoring Dashboard — container only. Builds the tab strip, wires the
|
|
auto-refresh timer and language-change retranslation, and forwards nav-rail
|
|
sub-tab selection. Each sub-tab is its own class under ``tabs/``; this class
|
|
owns no display logic of its own beyond assembling and refreshing them.
|
|
|
|
Public API preserved exactly for ``app.py`` (which cannot be modified):
|
|
``MonitoringTab(ctx, cowork=None, structure=None, task_scheduler=None)``,
|
|
the ``status_message`` signal, ``select_subtab(index)``, ``nav_subtabs()``,
|
|
``hide_tab_bar()``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import List
|
|
|
|
from PySide6.QtCore import QTimer, Signal
|
|
from PySide6.QtWidgets import QHBoxLayout, QLabel, QTabWidget, QVBoxLayout, QWidget
|
|
|
|
from ...core import audit_log
|
|
from ...i18n import on_language_changed, tr
|
|
from ...state import AppContext
|
|
from .tabs.action_logs_tab import ActionLogsTab
|
|
from .tabs.agent_status_tab import AgentStatusTab
|
|
from .tabs.mcp_tab import McpTab
|
|
from .tabs.overview_tab import OverviewTab
|
|
from .tabs.security_events_tab import SecurityEventsTab
|
|
|
|
_REFRESH_MS = 3000
|
|
# Comfortably larger than any realistic audit-log size — the event tables
|
|
# have never had pagination controls, so every tab still shows "all matching
|
|
# events" exactly like before; MonitoringQueryService's pagination support
|
|
# is exercised for real here, just not surfaced as UI (yet).
|
|
_UNBOUNDED_PAGE_SIZE = 100_000
|
|
|
|
|
|
class MonitoringTab(QWidget):
|
|
status_message = Signal(str)
|
|
|
|
def __init__(self, ctx: AppContext, cowork=None, structure=None, task_scheduler=None):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self._cowork = cowork
|
|
self._structure = structure
|
|
self._task_scheduler = task_scheduler
|
|
|
|
root = QVBoxLayout(self)
|
|
head = QHBoxLayout()
|
|
self._title = QLabel()
|
|
self._title.setStyleSheet("font-weight:700; font-size:15px;")
|
|
head.addWidget(self._title)
|
|
head.addStretch(1)
|
|
root.addLayout(head)
|
|
|
|
self.tabs = QTabWidget()
|
|
root.addWidget(self.tabs, 1)
|
|
|
|
visible = self._tab_visible
|
|
|
|
self.overview_tab = OverviewTab(
|
|
ctx, on_status_message=self.status_message.emit,
|
|
on_settings_changed=self.refresh,
|
|
on_view_all_action_logs=self._show_action_logs_tab,
|
|
action_logs_tab_visible=visible("action_logs"))
|
|
self.tabs.addTab(self.overview_tab, "")
|
|
|
|
self.security_tab = SecurityEventsTab(ctx, on_refresh_all=self.refresh)
|
|
if visible("security_events"):
|
|
self.tabs.addTab(self.security_tab, "")
|
|
self.mcp_tab = McpTab(ctx, on_refresh_all=self.refresh)
|
|
if visible("mcp_history"):
|
|
self.tabs.addTab(self.mcp_tab, "")
|
|
self.action_tab = ActionLogsTab(ctx, on_refresh_all=self.refresh)
|
|
if visible("action_logs"):
|
|
self.tabs.addTab(self.action_tab, "")
|
|
|
|
self.status_tab = AgentStatusTab(
|
|
ctx, on_refresh_all=self.refresh,
|
|
cowork=cowork, structure=structure, task_scheduler=task_scheduler)
|
|
if visible("agent_status"):
|
|
self.tabs.addTab(self.status_tab, "")
|
|
|
|
# ---- Agents Admin (catalog: assign a role + pinned model per agent) --
|
|
from ...ui.agents_admin_tab import AgentsAdminTab
|
|
self.agents_admin_tab = AgentsAdminTab(ctx)
|
|
if visible("agents_admin"):
|
|
self.tabs.addTab(self.agents_admin_tab, "")
|
|
|
|
# ---- Tools (govern built-in tools + Connectors/MCP in one place) -----
|
|
from ...ui.tools_admin_tab import ToolsAdminTab
|
|
self.tools_admin_tab = ToolsAdminTab(ctx)
|
|
if visible("tools_admin"):
|
|
self.tabs.addTab(self.tools_admin_tab, "")
|
|
|
|
# ---- Icons (browse built-in icons + add custom icons for agents/flows) --
|
|
from ...ui.icons_admin_tab import IconsAdminTab
|
|
self.icons_admin_tab = IconsAdminTab(ctx)
|
|
self.tabs.addTab(self.icons_admin_tab, "")
|
|
|
|
self.tabs.setCurrentIndex(0)
|
|
|
|
self._timer = QTimer(self)
|
|
# Only the Overview cards auto-refresh on this tick — Security, MCP,
|
|
# Action Logs, Agent Status (and the admin tabs) are read-only tables
|
|
# a background re-sort would otherwise disturb mid-interaction; the
|
|
# user refreshes them explicitly via a "Refresh" button.
|
|
self._timer.setInterval(_REFRESH_MS)
|
|
self._timer.timeout.connect(self._auto_refresh)
|
|
self._timer.start()
|
|
|
|
on_language_changed(self._retranslate)
|
|
self.refresh()
|
|
|
|
# ---- nav integration: sub-tabs driven from the left nav rail ------------
|
|
def nav_subtabs(self):
|
|
"""(label, index, icon_name) for each sub-tab — the left nav lists
|
|
these as children under 'Monitoring'."""
|
|
by_widget = {self.agents_admin_tab: "robot", self.tools_admin_tab: "wrench",
|
|
self.icons_admin_tab: "star"}
|
|
for attr, name in (("security_tab", "shield"), ("mcp_tab", "plug"),
|
|
("action_tab", "bolt"), ("status_tab", "monitor")):
|
|
w = getattr(self, attr, None)
|
|
if w is not None:
|
|
by_widget[w] = name
|
|
out = []
|
|
for i in range(self.tabs.count()):
|
|
out.append((self.tabs.tabText(i), i, by_widget.get(self.tabs.widget(i), "dashboard")))
|
|
return out
|
|
|
|
def select_subtab(self, index: int) -> None:
|
|
if 0 <= index < self.tabs.count():
|
|
self.tabs.setCurrentIndex(index)
|
|
|
|
def hide_tab_bar(self) -> None:
|
|
"""Hide the in-content tab strip; the nav rail drives the sub-tabs."""
|
|
self.tabs.tabBar().hide()
|
|
|
|
def _show_action_logs_tab(self) -> None:
|
|
self.tabs.setCurrentIndex(self.tabs.indexOf(self.action_tab))
|
|
|
|
def _tab_visible(self, key: str) -> bool:
|
|
return self.ctx.role == "admin" or bool(self.ctx.config.monitoring_visibility.get(key, True))
|
|
|
|
def _set_tab_text_if_present(self, widget, text: str) -> None:
|
|
idx = self.tabs.indexOf(widget)
|
|
if idx >= 0:
|
|
self.tabs.setTabText(idx, text)
|
|
|
|
# ---- i18n ---------------------------------------------------------------
|
|
def _retranslate(self) -> None:
|
|
self._title.setText(tr("monitoring.title"))
|
|
if self.tabs.count():
|
|
self.tabs.setTabText(0, tr("monitoring.tab_overview"))
|
|
self._set_tab_text_if_present(self.security_tab, tr("monitoring.tab_security"))
|
|
self._set_tab_text_if_present(self.mcp_tab, tr("monitoring.tab_mcp"))
|
|
self._set_tab_text_if_present(self.action_tab, tr("monitoring.tab_actions"))
|
|
self._set_tab_text_if_present(self.status_tab, tr("monitoring.tab_agents"))
|
|
self._set_tab_text_if_present(self.agents_admin_tab, tr("monitoring.tab_agents_admin"))
|
|
self._set_tab_text_if_present(self.tools_admin_tab, tr("monitoring.tab_tools"))
|
|
self._set_tab_text_if_present(self.icons_admin_tab, tr("monitoring.tab_icons"))
|
|
|
|
self.overview_tab.retranslate()
|
|
self.security_tab.retranslate()
|
|
self.mcp_tab.retranslate()
|
|
self.action_tab.retranslate()
|
|
self.status_tab.retranslate()
|
|
|
|
self.refresh()
|
|
|
|
# ---- refresh -------------------------------------------------------------
|
|
def refresh(self) -> None:
|
|
"""Full refresh — Overview cards plus every table. Wired to the
|
|
top-of-page and per-section "Refresh" buttons, called once at
|
|
startup/language-change, but NOT to the auto-refresh timer (see
|
|
``_auto_refresh``)."""
|
|
events = self._load_events()
|
|
self._apply_events_to_event_tabs(events)
|
|
self.status_tab.refresh()
|
|
self.overview_tab.refresh(events)
|
|
|
|
def _auto_refresh(self) -> None:
|
|
"""3-second timer tick — Overview cards only (see ``refresh``)."""
|
|
self.overview_tab.refresh(self._load_events())
|
|
|
|
def _load_events(self) -> List[dict]:
|
|
shared_dir = self.ctx.config.shared_dir
|
|
if shared_dir:
|
|
from ...core import telemetry_shared
|
|
shared_events = telemetry_shared.load_shared_audit_events(shared_dir)
|
|
if shared_events:
|
|
return shared_events
|
|
return audit_log.load_events()
|
|
|
|
def _apply_events_to_event_tabs(self, events: List[dict]) -> None:
|
|
"""Filters the ALREADY-LOADED event list (see ``_load_events`` — one
|
|
shared local-or-shared decision per refresh, not one per tab) three
|
|
ways via :class:`MonitoringQueryService`, mirroring what each
|
|
``EventTable.set_events`` used to receive directly."""
|
|
from ...application.monitoring.dto.audit_event_dto import AuditEventDTO
|
|
from ...application.monitoring.monitoring_query_service import MonitoringQueryService
|
|
from ...application.monitoring.repository.audit_event_repository import (
|
|
InMemoryAuditEventRepository,
|
|
)
|
|
|
|
repository = InMemoryAuditEventRepository([AuditEventDTO.from_raw(e) for e in events])
|
|
service = MonitoringQueryService(repository)
|
|
|
|
def _events_for(kind) -> List[dict]:
|
|
page = service.query(kind=kind, page_size=_UNBOUNDED_PAGE_SIZE)
|
|
return [e.to_dict() for e in page.items]
|
|
|
|
self.security_tab.set_events(_events_for("security_block"))
|
|
self.mcp_tab.set_events(_events_for("mcp_call"))
|
|
self.action_tab.set_events(_events_for(None))
|