Files
cowork-local/presentation/monitoring/monitoring_tab.py
T
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
2026-08-31 05:15:13 +00:00

293 lines
14 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):
"""Vỏ màn Giám sát: dải 8 tab và phần dữ liệu dùng chung cho chúng.
Nhật ký kiểm toán được đọc MỘT lần mỗi nhịp làm mới rồi chia cho các tab,
nên quyết định "đọc từ nguồn cục bộ hay nguồn chia sẻ" chỉ xảy ra đúng một
lần chứ không phải ở từng tab.
"""
status_message = Signal(str)
# ---- cầu tương thích sau khi tách 8 tab (R08-T08) --------------------
# Trước khi tách, 55 control của Tổng quan treo thẳng trên MonitoringTab với
# tiền tố ov_. Tách xong mỗi cái về đúng tab/thẻ của nó và rụng tiền tố.
#
# tools/check_controls_alive.py so với mốc git 291a611 và đòi cả 55 cái phải
# còn tới được — đó chính là việc của nó: bắt control biến mất trong lúc bóc
# tách. Lần này control còn đủ, chỉ đổi chỗ ở, nên bắc cầu theo tiền tố.
#
# Định tuyến theo tiền tố chứ không dò mờ: overview_tab và permissions_card
# có những tên trùng nhau (network_lbl nằm ở cả hai — một cái là mức dùng
# mạng, một cái là quyền truy cập mạng). Dò mờ vớ nhầm cái đầu tiên tìm thấy.
_OV_TIEN_TO = (
("ov_perm_", lambda s: s.overview_tab.sandbox_card.permissions_card),
("ov_sbx_", lambda s: s.overview_tab.sandbox_card),
("ov_price_", lambda s: s.overview_tab.pricing_panel),
("ov_pricing_", lambda s: s.overview_tab.pricing_panel),
("ov_", lambda s: s.overview_tab),
)
#: Ba hộp nhóm: bản thân widget con CHÍNH LÀ hộp đó, không phải thuộc tính.
_OV_CHINH_NO = {
"ov_pricing_group": lambda s: s.overview_tab.pricing_panel,
"ov_sandbox_details_group": lambda s: s.overview_tab.sandbox_card,
"ov_permissions_group": lambda s: s.overview_tab.sandbox_card.permissions_card,
}
#: Vài control không mang tiền tố ov_ nhưng cũng đã dời đi.
_KHAC = {
"status_table": lambda s: s.status_tab.table,
}
def __getattr__(self, name):
"""Cầu tương thích cho 55 control cũ mang tiền tố ``ov_``.
R08-T08 chuyển chúng về đúng tab/thẻ của mình và bỏ tiền tố, nhưng các
checker trong ``tools/`` vẫn hỏi theo tên cũ. Định tuyến theo TIỀN TỐ chứ
không dò mờ: ``overview_tab`` và ``permissions_card`` có tên trùng nhau
(``network_lbl`` ở cả hai — một cái là mức dùng mạng, một cái là quyền truy
cập mạng), dò mờ sẽ vớ nhầm cái tìm thấy trước.
"""
lay_khac = self._KHAC.get(name)
if lay_khac is not None:
return lay_khac(self)
# Qt gọi __getattr__ rất nhiều lúc khởi tạo; chặn sớm cho rẻ.
if not name.startswith("ov_"):
raise AttributeError(name)
lay = self._OV_CHINH_NO.get(name)
if lay is not None:
return lay(self)
for tien_to, chu in self._OV_TIEN_TO:
if name.startswith(tien_to):
try:
return getattr(chu(self), name[len(tien_to):])
except AttributeError:
continue
raise AttributeError(name)
def __init__(self, ctx: AppContext, cowork=None, structure=None, task_scheduler=None):
"""Ghép các tab con của màn Giám sát.
Các widget được truyền vào đều có thể là None (test dựng tab lẻ) — mọi chỗ
dùng phải kiểm trước.
"""
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:
"""Chuyển sang tab con thứ ``index`` (thanh menu bên trái gọi vào đây)."""
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:
"""Nhảy tới tab Nhật ký hành động (dùng khi bấm vào phần trích ở Tổng quan)."""
self.tabs.setCurrentIndex(self.tabs.indexOf(self.action_tab))
def _tab_visible(self, key: str) -> bool:
"""Tab này có được hiện không: admin thấy hết, còn lại theo cấu hình
``monitoring_visibility``.
"""
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:
"""Đổi nhãn một tab, bỏ qua nếu tab đó đang bị ẩn theo phân quyền."""
idx = self.tabs.indexOf(widget)
if idx >= 0:
self.tabs.setTabText(idx, text)
# ---- i18n ---------------------------------------------------------------
def _retranslate(self) -> None:
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề và nhãn từng tab."""
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]:
"""Đọc nhật ký kiểm toán, ưu tiên nguồn chia sẻ rồi mới tới nguồn cục bộ.
Có cấu hình thư mục chia sẻ VÀ đọc ra được dữ liệu thì dùng nó, để cả đội
nhìn chung một bức tranh; rỗng thì rơi về nhật ký của máy này.
"""
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]:
"""Lấy toàn bộ sự kiện của một loại, không phân trang."""
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))