feat: redesign StatusTab UI with bento-grid layout and add CSV metadata editing capabilities

This commit is contained in:
jack
2026-07-07 16:41:40 -07:00
parent 92f130b3bd
commit b6903af625
8 changed files with 571 additions and 443 deletions
@@ -73,6 +73,15 @@ class DeviceController(QObject):
self._activity_log: list[str] = []
self._raw_bytes: Optional[bytes] = None
self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv")
# TODO P1.2: accept ctor param license_lookup: Callable[[str], str]
# (default lambda s: "") and add:
# def _wafer_license_level(self) -> str:
# return self._license_lookup(self._last_wafer_info.get("serialNumber", ""))
# plus @Slot(result=bool) waferLicensed() for QML (used by P2.2).
# THINKING: single shared guard — per-call-site checks are the
# sibling-caller bug (fix read, forget erase). Empty serial → ""
# → denied = safe default. Called by P2.1.
# See docs/pending/license-gating-plan.md §1.2.
self._last_wafer_info: dict[str, Any] = {}
self._last_csv_path: str = ""
self._selected_port: str = ""
@@ -116,6 +125,11 @@ class DeviceController(QObject):
"""Whether a hardware operation is currently running."""
return self._operation_in_progress
@Property(str, notify=activityLogUpdated)
def activityLog(self) -> str:
"""Full activity log text; survives QML tab reloads."""
return "\n".join(self._activity_log)
@Property(str, notify=activityLogUpdated)
def saveDataDir(self) -> str:
"""Directory where parsed CSV data files are saved."""
@@ -324,6 +338,7 @@ class DeviceController(QObject):
self._graph_view.resetChart()
self._activity_log.clear()
self.detectResult.emit(None)
self.selectedPortChanged.emit()
self.parsedDataReady.emit({"success": False})
self._set_operation_progress(True)
@@ -349,20 +364,27 @@ class DeviceController(QObject):
self, port: Optional[str], info: Optional[WaferInfo]
) -> None:
"""Main-thread completion handler for detectWafer."""
if info is not None:
self._selected_port = port or ""
self._set_connection_status("Connected")
wafer_dict = self._wafer_info_to_dict(info)
self._last_wafer_info = wafer_dict
self.detectResult.emit(wafer_dict)
self._save_status()
else:
self._selected_port = ""
self._set_connection_status("Disconnected")
self._append_log("No wafer detected on any port")
self._last_wafer_info = {}
self.detectResult.emit(None)
self._set_operation_progress(False)
try:
if info is not None:
# Build the dict (can raise) before declaring success, so a
# bad payload can't leave status="Connected" while
# waferDetected stays false and Read/Erase stay greyed.
wafer_dict = self._wafer_info_to_dict(info)
self._selected_port = port or ""
self._last_wafer_info = wafer_dict
self._set_connection_status("Connected")
self.detectResult.emit(wafer_dict)
self.selectedPortChanged.emit()
self._save_status()
else:
self._selected_port = ""
self._set_connection_status("Disconnected")
self._append_log("No wafer detected on any port")
self._last_wafer_info = {}
self.detectResult.emit(None)
self.selectedPortChanged.emit()
finally:
self._set_operation_progress(False)
@Slot()
@slot_error_boundary
@@ -377,6 +399,16 @@ class DeviceController(QObject):
self._append_log("Already busy — ignoring read request")
return
# TODO P2.1: license guard —
# if self._wafer_license_level() == "":
# self._append_log("Wafer not licensed — load its license .bin (About dialog)")
# self.readResult.emit({"error": "Wafer not licensed"})
# return
# THINKING: this slot is the trust boundary; the QML enabled
# binding (P2.2) is UX only. Reuses existing readResult error
# shape → zero new signals. Depends on P1.2. Same guard goes in
# eraseMemory (emit eraseResult there). See plan §2.1.
if not self._selected_port:
self._append_log("No wafer detected — run Detect Wafer first")
self.readResult.emit({"error": "No port — detect wafer first"})
@@ -436,6 +468,9 @@ class DeviceController(QObject):
if self._operation_in_progress:
self._append_log("Already busy — ignoring erase request")
return
# TODO P2.1: same license guard as readMemoryAsync, but emit
# self.eraseResult.emit({"success": False}) on denial. See plan §2.1.
self._set_operation_progress(True)
self._set_connection_status("Erasing...")
self.portsUpdated.emit(self._service.enumerate_ports())
+9 -10
View File
@@ -71,17 +71,16 @@ class FileBrowser(QObject):
self._show_error(f"CSV file was not found:\n{file_path}")
return
# ponytail: row is absent when saving metadata for a CSV just read from
# the wafer (not in the browser listing) — still write the sidecar.
row = self._find_row(file_path)
if row is None:
self._show_error("The selected CSV row is no longer available.")
return
row["wafer"] = wafer
row["date"] = date
row["chamber"] = chamber
row["notes"] = notes
row["selected"] = selected
row["masterType"] = master_type
if row is not None:
row["wafer"] = wafer
row["date"] = date
row["chamber"] = chamber
row["notes"] = notes
row["selected"] = selected
row["masterType"] = master_type
payload = {
"wafer": wafer,