fix(ui): Monitoring Overview now reads like the wireframe, and the rail stops overlapping

Two problems, both only visible at the window size this machine actually opens
(1024×571 on a 1280×672 work area):

  * The rail drew the "GẦN ĐÂY" heading on top of the last nav row. nav and the
    bottom group have fixed heights, so a short window put the entire squeeze on
    RECENTS; once that hit zero the layout had nowhere left to give. The
    destinations and RECENTS now scroll together inside their own area, while
    the bottom group, Settings and the account row stay pinned.

  * Overview did not look like the drawing. Fixed, in the order the page draws
    them: sections are quiet caps headings with flat content instead of six
    bordered boxes; the four resource metrics sit ACROSS one row rather than
    stacked four deep; and "Hoạt động gần đây" moved from second place to the
    bottom beside the audit log, so the reading order is cost → machine →
    permissions → prices → what happened. At 1280×760 the page now reaches the
    price table on first screen; before it showed one and a half sections.

Two deliberate differences from the drawing, both because matching it would
remove information: the wireframe shows four KPI tiles where the app has five
real metrics plus the budget entry card, and it writes resources as one text
line where the app keeps the CPU/RAM gauges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
NamPDT
2026-08-17 14:25:18 +09:00
co-authored by Claude Opus 5
parent b6dee044c9
commit f15557483f
4 changed files with 80 additions and 25 deletions
+25 -5
View File
@@ -10,8 +10,9 @@ from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QGuiApplication, QIcon from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu, QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu,
QPushButton, QSizePolicy, QSplitter, QStackedWidget, QSystemTrayIcon, QPushButton, QScrollArea, QSizePolicy, QSplitter, QStackedWidget,
QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, QSystemTrayIcon, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget,
) )
from . import APP_NAME, DISPLAY_NAME, __version__ from . import APP_NAME, DISPLAY_NAME, __version__
@@ -238,17 +239,36 @@ class MainWindow(QMainWindow):
self.workspace.projects_changed.connect(self._sync_rail_project) self.workspace.projects_changed.connect(self._sync_rail_project)
self._syncing_rail_project = False self._syncing_rail_project = False
self._sync_rail_project() self._sync_rail_project()
nvl.addWidget(self.nav, 0) # The destinations and RECENTS scroll together; the bottom group, the
# Settings button and the account row stay pinned below them.
#
# Without this the rail simply ran out of room on a short window (a
# 1280×720 laptop leaves ~570px here): nav and the bottom group have
# fixed heights, so the squeeze fell entirely on RECENTS, and once that
# hit zero the layout drew the "GẦN ĐÂY" heading straight over the last
# nav row.
self._nav_scroll = QScrollArea()
self._nav_scroll.setObjectName("navScroll")
self._nav_scroll.setWidgetResizable(True)
self._nav_scroll.setFrameShape(QScrollArea.NoFrame)
self._nav_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_body = QWidget()
sv = QVBoxLayout(scroll_body)
sv.setContentsMargins(0, 0, 0, 0)
sv.setSpacing(0)
sv.addWidget(self.nav, 0)
# RECENTS — the threads of the project named in the picker above, right # RECENTS — the threads of the project named in the picker above, right
# where Claude puts them. A shortcut only: the full History panel (search, # where Claude puts them. A shortcut only: the full History panel (search,
# filters, pin, bulk delete, context menu) stays exactly where it is, and # filters, pin, bulk delete, context menu) stays exactly where it is, and
# "all projects…" at the end of this list opens it. # "all projects…" at the end of this list opens it.
self.nav_recents_hdr = QLabel(tr("app.nav.recents")) self.nav_recents_hdr = QLabel(tr("app.nav.recents"))
self.nav_recents_hdr.setObjectName("navSectionHdr") self.nav_recents_hdr.setObjectName("navSectionHdr")
nvl.addWidget(self.nav_recents_hdr) sv.addWidget(self.nav_recents_hdr)
self.nav_recents = self._new_nav_tree("navRecents") self.nav_recents = self._new_nav_tree("navRecents")
self.nav_recents.itemClicked.connect(self._on_rail_recent) self.nav_recents.itemClicked.connect(self._on_rail_recent)
nvl.addWidget(self.nav_recents, 1) sv.addWidget(self.nav_recents, 1)
self._nav_scroll.setWidget(scroll_body)
nvl.addWidget(self._nav_scroll, 1)
# Bottom-pinned group: the places you visit occasionally, kept out of the # Bottom-pinned group: the places you visit occasionally, kept out of the
# way of the ones you live in. A hairline (styled via #navrailBottom in # way of the ones you live in. A hairline (styled via #navrailBottom in
# theme.py) separates the two lists. # theme.py) separates the two lists.
+13
View File
@@ -377,6 +377,19 @@ QWidget#navWrap QTreeWidget::item:disabled { color: $text_faint; }
/* The pinned bottom group (Dashboard, Monitoring) — one hairline separates it /* The pinned bottom group (Dashboard, Monitoring) — one hairline separates it
from the list above so "occasional" reads apart from "everyday". */ from the list above so "occasional" reads apart from "everyday". */
QTreeWidget#navrailBottom { border-top: 1px solid $nav_border; } QTreeWidget#navrailBottom { border-top: 1px solid $nav_border; }
QScrollArea#navScroll { background: transparent; border: none; }
QScrollArea#navScroll > QWidget > QWidget { background: transparent; }
/* Monitoring ▸ Overview reads as titled sections down one column, the way the
audit page draws it — a quiet caps heading with the content flat underneath,
not six bordered boxes competing with the cards inside them. */
QGroupBox#monSection {
background: transparent; border: none; margin-top: 16px;
padding: 6px 0 0 0; font-weight: 700;
}
QGroupBox#monSection::title {
subcontrol-origin: margin; subcontrol-position: top left; left: 2px; top: 0;
padding: 0; color: $text_faint; font-size: 11px; letter-spacing: 0.5px;
}
/* Segmented control: two-to-four choices shown side by side (language, theme) /* Segmented control: two-to-four choices shown side by side (language, theme)
instead of a drop-list you must open to see what the options even are. */ instead of a drop-list you must open to see what the options even are. */
QPushButton#segItem { QPushButton#segItem {
+5 -3
View File
@@ -24,9 +24,11 @@ from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # no
# The rail, top to bottom, as the audit page's rail() helper draws it. # The rail, top to bottom, as the audit page's rail() helper draws it.
RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"] RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"]
RAIL_BOTTOM = ["Dashboard", "Giám sát"] RAIL_BOTTOM = ["Dashboard", "Giám sát"]
# Monitoring ▸ Tổng quan, as the wireframe stacks it. # Monitoring ▸ Tổng quan, in the order the wireframe stacks it: what it cost →
MON_ORDER = ["ov_usage_group", "ov_activity_group", "ov_resource_group", # what the machine is doing → what the agent may touch → per-model prices →
"ov_sandbox_details_group", "ov_pricing_group", "ov_audit_group"] # what actually happened.
MON_ORDER = ["ov_usage_group", "ov_resource_group", "ov_sandbox_details_group",
"ov_pricing_group", "ov_activity_group", "ov_audit_group"]
def main() -> int: def main() -> int:
+37 -17
View File
@@ -417,6 +417,7 @@ class MonitoringTab(QWidget):
# ---- Token Usage & Cost -------------------------------------------- # ---- Token Usage & Cost --------------------------------------------
self.ov_usage_group = QGroupBox() self.ov_usage_group = QGroupBox()
self.ov_usage_group.setObjectName("monSection")
usage_lay = QGridLayout(self.ov_usage_group) usage_lay = QGridLayout(self.ov_usage_group)
usage_lay.setSpacing(8) usage_lay.setSpacing(8)
self.ov_usage_total = StatCard() self.ov_usage_total = StatCard()
@@ -447,41 +448,54 @@ class MonitoringTab(QWidget):
# ---- Recent Activity ---------------------------------------------- # ---- Recent Activity ----------------------------------------------
self.ov_activity_group = QGroupBox() self.ov_activity_group = QGroupBox()
self.ov_activity_group.setObjectName("monSection")
act_lay = QVBoxLayout(self.ov_activity_group) act_lay = QVBoxLayout(self.ov_activity_group)
self.ov_activity_lbl = QLabel() self.ov_activity_lbl = QLabel()
self.ov_activity_lbl.setWordWrap(True) self.ov_activity_lbl.setWordWrap(True)
self.ov_activity_lbl.setTextFormat(Qt.RichText) self.ov_activity_lbl.setTextFormat(Qt.RichText)
act_lay.addWidget(self.ov_activity_lbl) act_lay.addWidget(self.ov_activity_lbl)
left.addWidget(self.ov_activity_group) # added near the bottom, beside the audit log — see below
# ---- Resource Usage ------------------------------------------------- # ---- Resource Usage -------------------------------------------------
self.ov_resource_group = QGroupBox() self.ov_resource_group = QGroupBox()
res_lay = QVBoxLayout(self.ov_resource_group) self.ov_resource_group.setObjectName("monSection")
res_lay = QHBoxLayout(self.ov_resource_group)
res_lay.setSpacing(18)
def _bar_row(): def _bar_row():
row = QHBoxLayout() # One metric as a compact column: name, value, and the gauge under
lbl = QLabel(); lbl.setFixedWidth(70) # them. The bars stay — the wireframe writes this section as a
bar = QProgressBar(); bar.setFixedHeight(8); bar.setTextVisible(False) # single line, and dropping them would lose the at-a-glance load.
val = QLabel(); val.setFixedWidth(90); val.setAlignment(Qt.AlignRight) cell = QVBoxLayout()
row.addWidget(lbl); row.addWidget(bar, 1); row.addWidget(val) cell.setSpacing(2)
res_lay.addLayout(row) top = QHBoxLayout()
lbl = QLabel()
val = QLabel(); val.setAlignment(Qt.AlignRight)
top.addWidget(lbl); top.addStretch(1); top.addWidget(val)
bar = QProgressBar(); bar.setFixedHeight(6); bar.setTextVisible(False)
cell.addLayout(top); cell.addWidget(bar)
res_lay.addLayout(cell, 1)
return lbl, bar, val return lbl, bar, val
def _text_row(): def _text_row():
cell = QVBoxLayout()
cell.setSpacing(2)
row = QHBoxLayout() row = QHBoxLayout()
lbl = QLabel(); val = QLabel(); val.setAlignment(Qt.AlignRight) lbl = QLabel(); val = QLabel(); val.setAlignment(Qt.AlignRight)
row.addWidget(lbl); row.addStretch(1); row.addWidget(val) row.addWidget(lbl); row.addStretch(1); row.addWidget(val)
res_lay.addLayout(row) cell.addLayout(row)
cell.addSpacing(6)
res_lay.addLayout(cell, 1)
return lbl, val return lbl, val
self.ov_cpu_lbl, self.ov_cpu_bar, self.ov_cpu_val = _bar_row() self.ov_cpu_lbl, self.ov_cpu_bar, self.ov_cpu_val = _bar_row()
self.ov_mem_lbl, self.ov_mem_bar, self.ov_mem_val = _bar_row() self.ov_mem_lbl, self.ov_mem_bar, self.ov_mem_val = _bar_row()
self.ov_disk_lbl, self.ov_disk_val = _text_row() self.ov_disk_lbl, self.ov_disk_val = _text_row()
self.ov_network_lbl, self.ov_network_val = _text_row() self.ov_network_lbl, self.ov_network_val = _text_row()
res_lay.addStretch(1)
# ---- Model pricing (beside the CPU/resource group) ------------------ # ---- Model pricing (beside the CPU/resource group) ------------------
self.ov_pricing_group = QGroupBox() self.ov_pricing_group = QGroupBox()
self.ov_pricing_group.setObjectName("monSection")
pg = QVBoxLayout(self.ov_pricing_group) pg = QVBoxLayout(self.ov_pricing_group)
phdr = QHBoxLayout() phdr = QHBoxLayout()
self.ov_pricing_ccy_lbl = QLabel(); self.ov_pricing_ccy_lbl.setObjectName("hint") self.ov_pricing_ccy_lbl = QLabel(); self.ov_pricing_ccy_lbl.setObjectName("hint")
@@ -524,6 +538,7 @@ class MonitoringTab(QWidget):
# ---- Sandbox Details -------------------------------------------- # ---- Sandbox Details --------------------------------------------
self.ov_sandbox_details_group = QGroupBox() self.ov_sandbox_details_group = QGroupBox()
self.ov_sandbox_details_group.setObjectName("monSection")
sbx_lay = QVBoxLayout(self.ov_sandbox_details_group) sbx_lay = QVBoxLayout(self.ov_sandbox_details_group)
def _kv(): def _kv():
@@ -561,6 +576,7 @@ class MonitoringTab(QWidget):
# ---- Permissions ----------------------------------------------- # ---- Permissions -----------------------------------------------
self.ov_permissions_group = QGroupBox() self.ov_permissions_group = QGroupBox()
self.ov_permissions_group.setObjectName("monSection")
perm_lay = QVBoxLayout(self.ov_permissions_group) perm_lay = QVBoxLayout(self.ov_permissions_group)
def _pkv(): def _pkv():
@@ -584,8 +600,12 @@ class MonitoringTab(QWidget):
# ---- Model pricing — its own section, full width ------------------ # ---- Model pricing — its own section, full width ------------------
root.addWidget(self.ov_pricing_group) root.addWidget(self.ov_pricing_group)
# ---- What actually happened, last ---------------------------------
root.addWidget(self.ov_activity_group)
# ---- Audit Log ---------------------------------------------------- # ---- Audit Log ----------------------------------------------------
self.ov_audit_group = QGroupBox() self.ov_audit_group = QGroupBox()
self.ov_audit_group.setObjectName("monSection")
audit_lay = QVBoxLayout(self.ov_audit_group) audit_lay = QVBoxLayout(self.ov_audit_group)
self.ov_audit_lbl = QLabel() self.ov_audit_lbl = QLabel()
self.ov_audit_lbl.setWordWrap(True) self.ov_audit_lbl.setWordWrap(True)
@@ -645,12 +665,12 @@ class MonitoringTab(QWidget):
# A QGroupBox title treats "&" as a mnemonic marker, so "token & Chi # A QGroupBox title treats "&" as a mnemonic marker, so "token & Chi
# phí" rendered as "token _Chi phí". Double it to show a literal "&". # phí" rendered as "token _Chi phí". Double it to show a literal "&".
self.ov_usage_group.setTitle( self.ov_usage_group.setTitle(
tr("monitoring.overview_usage_title").replace("&", "&&")) tr("monitoring.overview_usage_title").upper().replace("&", "&&"))
self.ov_budget_card.apply_btn.setToolTip(tr("usage.budget_apply_tooltip")) self.ov_budget_card.apply_btn.setToolTip(tr("usage.budget_apply_tooltip"))
self.ov_budget_card.budget_spin.setToolTip(tr("usage.budget_spin_tooltip")) self.ov_budget_card.budget_spin.setToolTip(tr("usage.budget_spin_tooltip"))
self.ov_activity_group.setTitle(tr("monitoring.overview_activity_title")) self.ov_activity_group.setTitle(tr("monitoring.overview_activity_title").upper())
self.ov_resource_group.setTitle(tr("monitoring.overview_resource_title")) self.ov_resource_group.setTitle(tr("monitoring.overview_resource_title").upper())
self.security_page.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder")) self.security_page.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder"))
self.action_page.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder")) self.action_page.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder"))
self.ov_cpu_lbl.setText(tr("monitoring.overview_res_cpu")) self.ov_cpu_lbl.setText(tr("monitoring.overview_res_cpu"))
@@ -658,7 +678,7 @@ class MonitoringTab(QWidget):
self.ov_disk_lbl.setText(tr("monitoring.overview_res_disk")) self.ov_disk_lbl.setText(tr("monitoring.overview_res_disk"))
self.ov_network_lbl.setText(tr("monitoring.overview_res_network")) self.ov_network_lbl.setText(tr("monitoring.overview_res_network"))
# model pricing panel # model pricing panel
self.ov_pricing_group.setTitle(tr("monitoring.pricing_title")) self.ov_pricing_group.setTitle(tr("monitoring.pricing_title").upper())
self.ov_pricing_ccy_lbl.setText(tr("monitoring.pricing_currency")) self.ov_pricing_ccy_lbl.setText(tr("monitoring.pricing_currency"))
self.ov_price_import_btn.setText(tr("monitoring.pricing_import")) self.ov_price_import_btn.setText(tr("monitoring.pricing_import"))
self.ov_price_export_btn.setText(tr("monitoring.pricing_export")) self.ov_price_export_btn.setText(tr("monitoring.pricing_export"))
@@ -670,7 +690,7 @@ class MonitoringTab(QWidget):
tr("monitoring.pricing_col_maxout"), tr("monitoring.pricing_col_input"), tr("monitoring.pricing_col_maxout"), tr("monitoring.pricing_col_input"),
tr("monitoring.pricing_col_output")]) tr("monitoring.pricing_col_output")])
self.ov_sandbox_details_group.setTitle(tr("monitoring.overview_sandbox_details_title")) self.ov_sandbox_details_group.setTitle(tr("monitoring.overview_sandbox_details_title").upper())
self.ov_sbx_id_lbl.setText(tr("monitoring.overview_sandbox_id")) self.ov_sbx_id_lbl.setText(tr("monitoring.overview_sandbox_id"))
self.ov_sbx_status_lbl.setText(tr("monitoring.overview_status")) self.ov_sbx_status_lbl.setText(tr("monitoring.overview_status"))
self.ov_sbx_created_lbl.setText(tr("monitoring.overview_created")) self.ov_sbx_created_lbl.setText(tr("monitoring.overview_created"))
@@ -678,7 +698,7 @@ class MonitoringTab(QWidget):
self.ov_sbx_edit_btn.setText(tr("monitoring.overview_edit")) self.ov_sbx_edit_btn.setText(tr("monitoring.overview_edit"))
self.ov_sbx_net_lbl.setText(tr("monitoring.overview_network_label")) self.ov_sbx_net_lbl.setText(tr("monitoring.overview_network_label"))
self.ov_permissions_group.setTitle(tr("monitoring.overview_permissions_title")) self.ov_permissions_group.setTitle(tr("monitoring.overview_permissions_title").upper())
self.ov_perm_fs_lbl.setText(tr("monitoring.overview_perm_fs")) self.ov_perm_fs_lbl.setText(tr("monitoring.overview_perm_fs"))
self.ov_perm_fs_val.setText(tr("monitoring.overview_perm_fs_value")) self.ov_perm_fs_val.setText(tr("monitoring.overview_perm_fs_value"))
self.ov_perm_network_lbl.setText(tr("monitoring.overview_perm_network")) self.ov_perm_network_lbl.setText(tr("monitoring.overview_perm_network"))
@@ -688,7 +708,7 @@ class MonitoringTab(QWidget):
self.ov_perm_env_val.setText(tr("monitoring.overview_perm_env_value")) self.ov_perm_env_val.setText(tr("monitoring.overview_perm_env_value"))
self.ov_perm_edit_btn.setText(tr("monitoring.overview_edit")) self.ov_perm_edit_btn.setText(tr("monitoring.overview_edit"))
self.ov_audit_group.setTitle(tr("monitoring.overview_audit_title")) self.ov_audit_group.setTitle(tr("monitoring.overview_audit_title").upper())
self.ov_view_all_btn.setText(tr("monitoring.overview_view_all")) self.ov_view_all_btn.setText(tr("monitoring.overview_view_all"))
self.refresh() self.refresh()