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:
@@ -2383,6 +2383,11 @@ STRINGS: Dict[str, Dict[str, str]] = {
|
|||||||
"monitoring.col_role": {"en": "Agent Role", "ja": "エージェント役割", "vi": "Vai trò Agent"},
|
"monitoring.col_role": {"en": "Agent Role", "ja": "エージェント役割", "vi": "Vai trò Agent"},
|
||||||
"monitoring.col_name": {"en": "Action", "ja": "アクション", "vi": "Hành động"},
|
"monitoring.col_name": {"en": "Action", "ja": "アクション", "vi": "Hành động"},
|
||||||
"monitoring.col_result": {"en": "Result", "ja": "結果", "vi": "Kết quả"},
|
"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_detail": {"en": "Detail", "ja": "詳細", "vi": "Chi tiết"},
|
||||||
"monitoring.col_account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
|
"monitoring.col_account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
|
||||||
"monitoring.col_machine": {"en": "Machine", "ja": "マシン", "vi": "Máy"},
|
"monitoring.col_machine": {"en": "Machine", "ja": "マシン", "vi": "Máy"},
|
||||||
|
|||||||
+61
-19
@@ -74,24 +74,41 @@ class _EventTable(QTableWidget):
|
|||||||
every column header is click-to-sort (ascending/descending toggle; the
|
every column header is click-to-sort (ascending/descending toggle; the
|
||||||
Time column's ISO timestamps sort correctly as text)."""
|
Time column's ISO timestamps sort correctly as text)."""
|
||||||
|
|
||||||
def __init__(self):
|
# What each blocked action is, as a colour. Security events all record
|
||||||
super().__init__(0, 7)
|
# 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.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||||
self.setSelectionBehavior(QTableWidget.SelectRows)
|
self.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
self.verticalHeader().setVisible(False)
|
self.verticalHeader().setVisible(False)
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
header = self.horizontalHeader()
|
header = self.horizontalHeader()
|
||||||
header.setStretchLastSection(True)
|
header.setStretchLastSection(True)
|
||||||
for col in range(6):
|
for col in range(self.columnCount() - 1):
|
||||||
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
def retranslate(self) -> None:
|
def retranslate(self) -> None:
|
||||||
self.setHorizontalHeaderLabels([
|
cols = [tr("monitoring.col_time"), tr("monitoring.col_role"),
|
||||||
tr("monitoring.col_time"), tr("monitoring.col_role"),
|
tr("monitoring.col_account"), tr("monitoring.col_machine")]
|
||||||
tr("monitoring.col_account"), tr("monitoring.col_machine"),
|
if self._show_result:
|
||||||
tr("monitoring.col_name"), tr("monitoring.col_result"),
|
cols += [tr("monitoring.col_name"), tr("monitoring.col_result")]
|
||||||
tr("monitoring.col_detail"),
|
else:
|
||||||
])
|
cols += [tr("monitoring.col_action")]
|
||||||
|
cols += [tr("monitoring.col_detail")]
|
||||||
|
self.setHorizontalHeaderLabels(cols)
|
||||||
|
|
||||||
def set_events(self, events: List[dict]) -> None:
|
def set_events(self, events: List[dict]) -> None:
|
||||||
events = sorted(events, key=lambda e: e.get("ts", ""), reverse=True)[:_MAX_ROWS]
|
events = sorted(events, key=lambda e: e.get("ts", ""), reverse=True)[:_MAX_ROWS]
|
||||||
@@ -102,14 +119,28 @@ class _EventTable(QTableWidget):
|
|||||||
cells = [
|
cells = [
|
||||||
ev.get("ts", ""), agent_roles.label_for(ev.get("agent_role", "")),
|
ev.get("ts", ""), agent_roles.label_for(ev.get("agent_role", "")),
|
||||||
ev.get("account", "") or "—", ev.get("machine", "") or "—",
|
ev.get("account", "") or "—", ev.get("machine", "") or "—",
|
||||||
ev.get("name", ""), "",
|
ev.get("name", ""),
|
||||||
(ev.get("detail") or "")[:300],
|
|
||||||
]
|
]
|
||||||
|
if self._show_result:
|
||||||
|
cells.append("")
|
||||||
|
cells.append((ev.get("detail") or "")[:300])
|
||||||
|
pal = current_palette()
|
||||||
for col, text in enumerate(cells):
|
for col, text in enumerate(cells):
|
||||||
item = QTableWidgetItem(str(text))
|
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")
|
item.setIcon(icon("check", color=DOT_GREEN) if ev.get("ok")
|
||||||
else icon("close", color=DOT_RED))
|
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:
|
if is_admin_violation:
|
||||||
item.setBackground(QBrush(QColor(229, 72, 77, 60)))
|
item.setBackground(QBrush(QColor(229, 72, 77, 60)))
|
||||||
self.setItem(row, col, item)
|
self.setItem(row, col, item)
|
||||||
@@ -159,7 +190,9 @@ class MonitoringTab(QWidget):
|
|||||||
self.tabs.addTab(self._build_overview_page(), "")
|
self.tabs.addTab(self._build_overview_page(), "")
|
||||||
|
|
||||||
visible = self._tab_visible
|
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)
|
self.security_page = self._wrap_with_filter(self.security_table)
|
||||||
if visible("security_events"):
|
if visible("security_events"):
|
||||||
self.tabs.addTab(self.security_page, "")
|
self.tabs.addTab(self.security_page, "")
|
||||||
@@ -425,19 +458,24 @@ class MonitoringTab(QWidget):
|
|||||||
self.ov_usage_out = StatCard()
|
self.ov_usage_out = StatCard()
|
||||||
self.ov_usage_cache = StatCard()
|
self.ov_usage_cache = StatCard()
|
||||||
self.ov_usage_cost = 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_calls = StatCard()
|
||||||
self.ov_usage_cache, self.ov_usage_cost)):
|
for i, card in enumerate((self.ov_usage_total, self.ov_usage_cost,
|
||||||
|
self.ov_usage_calls)):
|
||||||
usage_lay.addWidget(card, 0, i)
|
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 —
|
# Budget: remaining/budget, direct entry, auto-warns red past 85% used —
|
||||||
# same box (and same usage.budget_* config) as the Dashboard's.
|
# same box (and same usage.budget_* config) as the Dashboard's.
|
||||||
self.ov_budget_card = BudgetCard()
|
self.ov_budget_card = BudgetCard()
|
||||||
self.ov_budget_card.apply_btn.setIcon(icon("check"))
|
self.ov_budget_card.apply_btn.setIcon(icon("check"))
|
||||||
self.ov_budget_card.apply_btn.clicked.connect(self._apply_budget)
|
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
|
# Equal stretch on every column — otherwise the grid sizes each column
|
||||||
# to its widest cell's natural content (Budget's longer "$X / $Y" value
|
# to its widest cell's natural content (Budget's longer "$X / $Y" value
|
||||||
# + entry row makes its column wider than the plain stat cards).
|
# + 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)
|
usage_lay.setColumnStretch(col, 1)
|
||||||
|
|
||||||
# Unit prices are NOT entered here anymore — the cost total is computed
|
# Unit prices are NOT entered here anymore — the cost total is computed
|
||||||
@@ -844,8 +882,12 @@ class MonitoringTab(QWidget):
|
|||||||
events = ut.load_events()
|
events = ut.load_events()
|
||||||
s = ut.summarize(events)
|
s = ut.summarize(events)
|
||||||
costs = ut.cost_usd_events(events, pricing)
|
costs = ut.cost_usd_events(events, pricing)
|
||||||
self.ov_usage_total.set(tr("dashboard.card_total"), fmt_tokens(s["total"]),
|
self.ov_usage_total.set(
|
||||||
tr("dashboard.card_turns", n=s["turns"]))
|
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"]),
|
self.ov_usage_in.set(tr("dashboard.card_in"), fmt_tokens(s["in"]),
|
||||||
ut.format_cost(costs["in"], pricing))
|
ut.format_cost(costs["in"], pricing))
|
||||||
self.ov_usage_out.set(tr("dashboard.card_out"), fmt_tokens(s["out"]),
|
self.ov_usage_out.set(tr("dashboard.card_out"), fmt_tokens(s["out"]),
|
||||||
|
|||||||
Reference in New Issue
Block a user