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:
+61
-19
@@ -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"]),
|
||||
|
||||
Reference in New Issue
Block a user