feat(monitoring): match sections 9 and 10 to the drawings

Section 10 (Sự kiện bảo mật) — the one item on that page that was in scope:
the "Kết quả" column always showed ✕, because every security_block is recorded
with ok=False, so the column distinguished nothing. It is replaced by an
"Hành động" column tinted by which rule actually fired (dangerous_command red,
path_outside_sandbox green, network_blocked blue, secret_in_output amber). The
other three items on that page are the ones marked "vượt phạm vi" — KPI tiles,
three filter drop-downs, pagination and the sliding detail panel — and two of
their four action types have no real log source yet.

Section 9 (Tổng quan) — the KPI row is four tiles as drawn: Tổng token · Tổng
chi phí · Lượt gọi · Ngân sách. Input/Output/Cache are NOT dropped: their
figures moved onto the token tile's sub-line ("Input 1.52M · Output 513.2K ·
Cache 641.9K"), so the screen still carries every number it did before while
the row reads as the drawing does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
NamPDT
2026-08-17 15:52:47 +09:00
co-authored by Claude Opus 5
parent 8cce8dc98b
commit 340fbaf56f
2 changed files with 66 additions and 19 deletions
+5
View File
@@ -2383,6 +2383,11 @@ STRINGS: Dict[str, Dict[str, str]] = {
"monitoring.col_role": {"en": "Agent Role", "ja": "エージェント役割", "vi": "Vai trò Agent"},
"monitoring.col_name": {"en": "Action", "ja": "アクション", "vi": "Hành động"},
"monitoring.col_result": {"en": "Result", "ja": "結果", "vi": "Kết quả"},
# Security Events shows WHICH rule fired instead of a result that is always
# the same — every security_block is recorded with ok=False.
"monitoring.col_action": {"en": "Action", "ja": "アクション", "vi": "Hành động"},
# The fourth KPI tile on Overview, as the wireframe labels it.
"monitoring.overview_calls": {"en": "Calls", "ja": "呼び出し", "vi": "Lượt gọi"},
"monitoring.col_detail": {"en": "Detail", "ja": "詳細", "vi": "Chi tiết"},
"monitoring.col_account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
"monitoring.col_machine": {"en": "Machine", "ja": "マシン", "vi": "Máy"},
+61 -19
View File
@@ -74,24 +74,41 @@ class _EventTable(QTableWidget):
every column header is click-to-sort (ascending/descending toggle; the
Time column's ISO timestamps sort correctly as text)."""
def __init__(self):
super().__init__(0, 7)
# What each blocked action is, as a colour. Security events all record
# ok=False, so the tick/cross column said the same thing on every row; the
# useful distinction is WHICH rule fired.
_ACTION_TINTS = {
"prompt": "accent",
"dangerous_command": "danger",
"run_command": "danger",
"install_package": "warning",
"path_outside_sandbox": "success",
"network_blocked": "accent",
"secret_in_output": "warning",
}
def __init__(self, show_result: bool = True):
# Security Events drops the result column entirely (see _ACTION_TINTS).
self._show_result = show_result
super().__init__(0, 7 if show_result else 6)
self.setEditTriggers(QTableWidget.NoEditTriggers)
self.setSelectionBehavior(QTableWidget.SelectRows)
self.verticalHeader().setVisible(False)
self.setSortingEnabled(True)
header = self.horizontalHeader()
header.setStretchLastSection(True)
for col in range(6):
for col in range(self.columnCount() - 1):
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
def retranslate(self) -> None:
self.setHorizontalHeaderLabels([
tr("monitoring.col_time"), tr("monitoring.col_role"),
tr("monitoring.col_account"), tr("monitoring.col_machine"),
tr("monitoring.col_name"), tr("monitoring.col_result"),
tr("monitoring.col_detail"),
])
cols = [tr("monitoring.col_time"), tr("monitoring.col_role"),
tr("monitoring.col_account"), tr("monitoring.col_machine")]
if self._show_result:
cols += [tr("monitoring.col_name"), tr("monitoring.col_result")]
else:
cols += [tr("monitoring.col_action")]
cols += [tr("monitoring.col_detail")]
self.setHorizontalHeaderLabels(cols)
def set_events(self, events: List[dict]) -> None:
events = sorted(events, key=lambda e: e.get("ts", ""), reverse=True)[:_MAX_ROWS]
@@ -102,14 +119,28 @@ class _EventTable(QTableWidget):
cells = [
ev.get("ts", ""), agent_roles.label_for(ev.get("agent_role", "")),
ev.get("account", "") or "—", ev.get("machine", "") or "—",
ev.get("name", ""), "",
(ev.get("detail") or "")[:300],
ev.get("name", ""),
]
if self._show_result:
cells.append("")
cells.append((ev.get("detail") or "")[:300])
pal = current_palette()
for col, text in enumerate(cells):
item = QTableWidgetItem(str(text))
if col == 5: # Result — green check / red close icon (no emoji)
if self._show_result and col == 5:
# Result — green check / red close icon (no emoji)
item.setIcon(icon("check", color=DOT_GREEN) if ev.get("ok")
else icon("close", color=DOT_RED))
if not self._show_result and col == 4:
# The action itself, tinted by which rule fired — this is
# the column the always-identical result column made way for.
tint = getattr(pal, self._ACTION_TINTS.get(
ev.get("name", ""), "text_muted"), pal.text_muted)
colour = QColor(tint)
item.setForeground(QBrush(colour))
soft = QColor(colour)
soft.setAlpha(38)
item.setBackground(QBrush(soft))
if is_admin_violation:
item.setBackground(QBrush(QColor(229, 72, 77, 60)))
self.setItem(row, col, item)
@@ -159,7 +190,9 @@ class MonitoringTab(QWidget):
self.tabs.addTab(self._build_overview_page(), "")
visible = self._tab_visible
self.security_table = _EventTable()
# Security events are always ok=False, so this table trades the
# tick/cross column for a tinted Action column (see _EventTable).
self.security_table = _EventTable(show_result=False)
self.security_page = self._wrap_with_filter(self.security_table)
if visible("security_events"):
self.tabs.addTab(self.security_page, "")
@@ -425,19 +458,24 @@ class MonitoringTab(QWidget):
self.ov_usage_out = StatCard()
self.ov_usage_cache = StatCard()
self.ov_usage_cost = StatCard()
for i, card in enumerate((self.ov_usage_total, self.ov_usage_in, self.ov_usage_out,
self.ov_usage_cache, self.ov_usage_cost)):
self.ov_usage_calls = StatCard()
for i, card in enumerate((self.ov_usage_total, self.ov_usage_cost,
self.ov_usage_calls)):
usage_lay.addWidget(card, 0, i)
# Kept alive and updated, but off the KPI row: their figures ride on the
# token tile's sub-line instead of taking three tiles of their own.
for hidden in (self.ov_usage_in, self.ov_usage_out, self.ov_usage_cache):
hidden.setVisible(False)
# Budget: remaining/budget, direct entry, auto-warns red past 85% used —
# same box (and same usage.budget_* config) as the Dashboard's.
self.ov_budget_card = BudgetCard()
self.ov_budget_card.apply_btn.setIcon(icon("check"))
self.ov_budget_card.apply_btn.clicked.connect(self._apply_budget)
usage_lay.addWidget(self.ov_budget_card, 0, 5, 2, 1)
usage_lay.addWidget(self.ov_budget_card, 0, 3)
# Equal stretch on every column — otherwise the grid sizes each column
# to its widest cell's natural content (Budget's longer "$X / $Y" value
# + entry row makes its column wider than the plain stat cards).
for col in range(6):
for col in range(4):
usage_lay.setColumnStretch(col, 1)
# Unit prices are NOT entered here anymore — the cost total is computed
@@ -844,8 +882,12 @@ class MonitoringTab(QWidget):
events = ut.load_events()
s = ut.summarize(events)
costs = ut.cost_usd_events(events, pricing)
self.ov_usage_total.set(tr("dashboard.card_total"), fmt_tokens(s["total"]),
tr("dashboard.card_turns", n=s["turns"]))
self.ov_usage_total.set(
tr("dashboard.card_total"), fmt_tokens(s["total"]),
f'{tr("dashboard.card_in")} {fmt_tokens(s["in"])} · '
f'{tr("dashboard.card_out")} {fmt_tokens(s["out"])} · '
f'{tr("dashboard.card_cache")} {fmt_tokens(s["cache"])}')
self.ov_usage_calls.set(tr("monitoring.overview_calls"), str(s["turns"]), "")
self.ov_usage_in.set(tr("dashboard.card_in"), fmt_tokens(s["in"]),
ut.format_cost(costs["in"], pricing))
self.ov_usage_out.set(tr("dashboard.card_out"), fmt_tokens(s["out"]),