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>
204 lines
8.5 KiB
Python
204 lines
8.5 KiB
Python
"""Round 2: does the built layout have the SHAPE the wireframes draw?
|
|
|
|
Round 1 asks "does the feature exist". A screen can pass that and still be laid
|
|
out wrongly — right widgets, wrong order, wrong side, wrong proportions. This
|
|
round measures real geometry against what the audit page's wireframes depict:
|
|
reading order of the rail, section order down Monitoring, which side each pane
|
|
is on, and the size relationships the design calls out (hero card, 26px dot).
|
|
|
|
Run: python tools/check_layout_geometry.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO.parent))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
|
|
|
# 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_BOTTOM = ["Dashboard", "Giám sát"]
|
|
# Monitoring ▸ Tổng quan, in the order the wireframe stacks it: what it cost →
|
|
# what the machine is doing → what the agent may touch → per-model prices →
|
|
# 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:
|
|
sandbox = _isolate_home()
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
app = QApplication([])
|
|
_load_fonts()
|
|
_freeze_schedulers()
|
|
|
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
|
|
|
from seed_demo_data import seed
|
|
seed()
|
|
|
|
from cowork_local.app import MainWindow
|
|
from cowork_local.i18n import set_language
|
|
from cowork_local.state import AppContext
|
|
|
|
set_language("vi")
|
|
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
|
win.resize(1600, 950)
|
|
win.show()
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
ws = win.workspace
|
|
fails: list[str] = []
|
|
|
|
def top_of(w, ref):
|
|
return w.mapTo(ref, w.rect().topLeft()).y()
|
|
|
|
def left_of(w, ref):
|
|
return w.mapTo(ref, w.rect().topLeft()).x()
|
|
|
|
# --- 1. rail: reading order, and the rail is on the LEFT ---------------
|
|
rows = [win.nav.topLevelItem(i).text(0) for i in range(win.nav.topLevelItemCount())]
|
|
bottom = [win.nav_bottom.topLevelItem(i).text(0)
|
|
for i in range(win.nav_bottom.topLevelItemCount())]
|
|
print(f"thanh menu : {rows}")
|
|
print(f"nhom day : {bottom}")
|
|
if rows != RAIL_ORDER:
|
|
fails.append(f"thu tu thanh menu lech: {rows} != {RAIL_ORDER}")
|
|
if bottom != RAIL_BOTTOM:
|
|
fails.append(f"thu tu nhom day lech: {bottom} != {RAIL_BOTTOM}")
|
|
rail_x = left_of(win._nav_wrap, win)
|
|
content_x = left_of(win.pages, win)
|
|
print(f"rail x={rail_x} · noi dung x={content_x}")
|
|
if rail_x >= content_x:
|
|
fails.append("rail khong nam ben trai noi dung")
|
|
|
|
# --- 2. rail header order: picker ABOVE the new-chat button ------------
|
|
py, by = top_of(win.nav_project, win), top_of(win.nav_new_chat, win)
|
|
ry = top_of(win.nav_recents, win)
|
|
ay = top_of(win._account_row, win)
|
|
print(f"bo chon y={py} · nut chat moi y={by} · GAN DAY y={ry} · tai khoan y={ay}")
|
|
if not (py < by < ry < ay):
|
|
fails.append("thu tu doc cua rail sai (bo chon → chat moi → GAN DAY → tai khoan)")
|
|
|
|
# --- 3. Monitoring: one column, sections in the drawn order ------------
|
|
win._goto(win._ROW_MONITORING, None)
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
mon = win._page_widgets[win._ROW_MONITORING]
|
|
tops = [(n, top_of(getattr(mon, n), mon)) for n in MON_ORDER if getattr(mon, n, None)]
|
|
lefts = {n: left_of(getattr(mon, n), mon) for n, _y in tops}
|
|
print("Monitoring, tu tren xuong:")
|
|
for n, y in tops:
|
|
print(f" {n:28} y={y:5} x={lefts[n]}")
|
|
if [n for n, _ in sorted(tops, key=lambda t: t[1])] != [n for n, _ in tops]:
|
|
fails.append("thu tu muc trong Monitoring khong khop ban ve")
|
|
# Sandbox and Permissions share a row; everything else is full width.
|
|
perm_y = top_of(mon.ov_permissions_group, mon)
|
|
sbx_y = top_of(mon.ov_sandbox_details_group, mon)
|
|
same_row = abs(perm_y - sbx_y) < 20
|
|
print(f"Sandbox | Quyen cung hang: {same_row}")
|
|
if not same_row:
|
|
fails.append("Sandbox va Quyen khong cung mot hang")
|
|
price_w = mon.ov_pricing_group.width()
|
|
res_w = mon.ov_resource_group.width()
|
|
print(f"bang gia rong {price_w}px · tai nguyen {res_w}px (deu tron be ngang)")
|
|
if price_w < res_w * 0.95:
|
|
fails.append("bang gia model khong chiem tron be ngang")
|
|
|
|
# --- 3b. Schedule: all seven lanes on screen, no horizontal scroll -----
|
|
win._goto(win._ROW_SCHEDULE, None)
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
sched = win._page_widgets[win._ROW_SCHEDULE]
|
|
from PySide6.QtWidgets import QScrollArea
|
|
lanes = list(sched.columns.values())
|
|
# The page holds more than one scroll area — take the one the lanes live in.
|
|
board = next(sa for sa in sched.findChildren(QScrollArea)
|
|
if sa.isAncestorOf(lanes[0]))
|
|
rightmost = max(left_of(c, board.widget()) + c.width() for c in lanes)
|
|
fits = rightmost <= board.viewport().width() + 2
|
|
print(f"Schedule: {len(lanes)} lane · mep phai x={rightmost} · "
|
|
f"khung rong {board.viewport().width()} · vua mot man = {fits}")
|
|
if len(lanes) != 7:
|
|
fails.append(f"chi co {len(lanes)} lane, thiet ke la 7")
|
|
if not fits:
|
|
fails.append(f"lane thu 7 nam ngoai man ({rightmost} > "
|
|
f"{board.viewport().width()}) — phai cuon ngang")
|
|
|
|
# --- 4. Dashboard: hero left, taller; supporting tiles in a 2x2 --------
|
|
win._goto(win._ROW_DASHBOARD, None)
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
dash = win._page_widgets[win._ROW_DASHBOARD]
|
|
hero, small = dash.card_cost, dash.card_total
|
|
print(f"the Chi phi : x={left_of(hero, dash)} cao={hero.height()} · "
|
|
f"the phu x={left_of(small, dash)} cao={small.height()}")
|
|
if left_of(hero, dash) >= left_of(small, dash):
|
|
fails.append("the Chi phi khong nam ben trai cac the phu")
|
|
if hero.height() < small.height() * 1.5:
|
|
fails.append("the Chi phi khong cao gap ruoi the phu")
|
|
row1 = top_of(dash.card_total, dash)
|
|
row2 = top_of(dash.card_out, dash)
|
|
print(f"the phu hang 1 y={row1} · hang 2 y={row2} (phai la 2 hang)")
|
|
if row2 <= row1:
|
|
fails.append("4 the phu khong xep 2x2")
|
|
|
|
# --- 5. Cowork: the dot clears the composer, dot is 26px --------------
|
|
win._goto(win._ROW_WORKSPACE, ws._cowork_tab_idx)
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
dock = win.help_agent
|
|
comp = ws._cowork.composer
|
|
dock_bottom = top_of(dock, win) + dock.height()
|
|
comp_top = top_of(comp, win)
|
|
print(f"cham {dock.width()}x{dock.height()} · day y={dock_bottom} · o nhap dinh y={comp_top}")
|
|
if dock.width() > 30:
|
|
fails.append(f"cham tro ly rong {dock.width()}px, thiet ke la 26px")
|
|
if dock_bottom > comp_top:
|
|
fails.append("cham tro ly de len o nhap")
|
|
if left_of(dock, win) + dock.width() > win.width():
|
|
fails.append("cham tro ly tran ra ngoai cua so")
|
|
|
|
# --- 6. Co4E: sidebar left, canvas middle, config right ---------------
|
|
win._goto(win._ROW_WORKSPACE, ws._co4e_tab_idx)
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
import cowork_local.ui.co4e_tab as co4e_mod
|
|
c4 = win.findChildren(co4e_mod.Co4ETab)[0]
|
|
xs = [c4._split.widget(i).x() for i in range(c4._split.count())]
|
|
print(f"Co4E 3 pane x = {xs}")
|
|
if xs != sorted(xs):
|
|
fails.append("thu tu 3 pane cua Co4E sai (trai → giua → phai)")
|
|
heads = [h.text() for h, _b, _s in c4._sections.values()]
|
|
print(f"cot sidebar: {heads}")
|
|
if len(heads) != 4:
|
|
fails.append(f"cot sidebar co {len(heads)} muc, ban ve ve 4")
|
|
|
|
print()
|
|
if fails:
|
|
print("*** LECH BO CUC ***")
|
|
for f in fails:
|
|
print(" " + f)
|
|
return 1
|
|
print("KET QUA VONG 2: hinh hoc khop ban ve")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_rc = main()
|
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
|
# caught its mutation. Leave immediately with the verdict instead.
|
|
sys.stdout.flush()
|
|
sys.stderr.flush()
|
|
os._exit(_rc)
|