import QtQuick import QtQuick.Layouts import QtQuick.Controls import QtQuick.Controls.impl import QtQuick.Dialogs import ISC // ===== Status Tab — Bento Grid Dashboard ===== // ROW 1: Connection Status (65%) | Cycles Completed (35%) // ROW 2: Wafer & Sensor + Directory (65%) | Total Runtime (35%) // ROW 3: Activity Log (100%, fills remaining) ColumnLayout { id: root anchors.fill: parent anchors.margins: Theme.panelPadding spacing: 8 // ── Reusable Inline Components ── component StatCard : Rectangle { property alias value: valueText.text property alias label: labelText.text property bool active: root.waferDetected property int valSize: active ? Theme.font3xl : Theme.font2xl Layout.fillWidth: true Layout.fillHeight: true color: Theme.cardBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusSm ColumnLayout { anchors.fill: parent anchors.margins: Theme.panelPadding spacing: 4 Text { id: labelText color: Theme.sideMutedText font.pixelSize: Theme.fontSm font.bold: true font.letterSpacing: 1.5 font.family: Theme.uiFontFamily } Text { id: valueText color: active ? Theme.headingColor : Theme.sideMutedText font.pixelSize: valSize font.weight: Font.Bold font.family: Theme.uiFontFamily Layout.fillHeight: true Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter fontSizeMode: Text.HorizontalFit minimumPixelSize: Theme.fontMd } } } // Stacked stat block: small label over big value, centered in its grid // quadrant so the card's height is used instead of leaving dead space. component GridCell : ColumnLayout { property alias label: labelText.text property alias value: valueText.text property bool active: root.waferDetected spacing: 2 Layout.fillWidth: true Layout.fillHeight: true Item { Layout.fillHeight: true } Text { id: labelText color: Theme.sideMutedText font.pixelSize: Theme.fontXs font.family: Theme.uiFontFamily Layout.fillWidth: true elide: Text.ElideRight } Text { id: valueText color: active ? Theme.headingColor : Theme.sideMutedText font.pixelSize: Theme.fontXl font.weight: Font.Bold font.family: Theme.uiFontFamily } Item { Layout.fillHeight: true } } // ── Aliases for external wiring ── property alias waferFamilyCode: waferInfoFamilyCell.value property alias waferSerialNumber: waferSerialCell.value property alias waferSensorCount: waferSensorsCell.value property alias waferRuntime: runtimeCard.value property alias waferCycles: waferCyclesCell.value property bool waferDetected: { var info = deviceController.lastWaferInfo; return info && info.length > 0 && info[0] !== ""; } property int dataRows: 0 property int dataCols: 0 property string csvPath: "" property bool dataParsed: false readonly property bool isConnected: deviceController.connectionStatus === "Connected" readonly property bool isBusy: deviceController.connectionStatus.endsWith("...") readonly property string portName: deviceController.selectedPort || "—" function cleanFolderUrl(url) { return decodeURIComponent(String(url).replace(/^file:\/\//, "")); } function currentFamilyCode() { var info = deviceController.lastWaferInfo; return info && info.length > 0 ? (info[0] || "") : ""; } function parseAndSavePendingRead() { deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || ""); } FolderDialog { id: saveDirDialog title: "Choose a folder to save." onAccepted: { var dir = root.cleanFolderUrl(selectedFolder); deviceController.setSaveDataDir(dir); file_browser.setCurrentDirectory(dir); root.parseAndSavePendingRead(); } } // Directory-only picker for the DIRECTORY card button — sets the save dir // without kicking off a parse/save of pending read data. FolderDialog { id: dirOnlyDialog title: "Choose a folder to save Data" onAccepted: { var dir = root.cleanFolderUrl(selectedFolder); deviceController.setSaveDataDir(dir); file_browser.setCurrentDirectory(dir); } } // Post-read metadata editor (C# parity: EditCSVMetadataDialog after read). // Cancel keeps the CSV, skips the sidecar — matching C# behavior loosely. Dialog { id: editCsvDialog parent: Overlay.overlay modal: true title: "Edit CSV Metadata" width: 460 anchors.centerIn: parent standardButtons: Dialog.Save | Dialog.Cancel onAccepted: file_browser.saveMetadata(root.csvPath, metaWafer.text, metaDate.text, metaChamber.text, metaNotes.text, false, "") ColumnLayout { anchors.fill: parent spacing: 6 Text { text: root.csvPath.split("/").pop() color: Theme.sideMutedText font.pixelSize: Theme.fontXs elide: Text.ElideMiddle Layout.fillWidth: true } Label { text: "Wafer" } TextField { id: metaWafer; Layout.fillWidth: true } Label { text: "Date" } TextField { id: metaDate; Layout.fillWidth: true } Label { text: "Chamber" } TextField { id: metaChamber; Layout.fillWidth: true } Label { text: "Notes" } TextField { id: metaNotes; Layout.fillWidth: true } } } // ═══════════════════════════════════════════════════════════════════════ // ROWS 1-2 (3-column bento): // [Connection Status] [Total Runtime ] [Wafer & Sensor] // [Directory (span 2) ] [ (rowspan) ] // ═══════════════════════════════════════════════════════════════════════ GridLayout { Layout.fillWidth: true Layout.preferredHeight: 188 Layout.maximumHeight: 188 columns: 3 rowSpacing: 8 columnSpacing: 8 // ── Connection Status: two cells wide, top-left ── Rectangle { Layout.columnSpan: 2 Layout.fillWidth: true Layout.fillHeight: true color: Theme.cardBackground border.color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor) border.width: 1 radius: Theme.radiusSm ColumnLayout { anchors.fill: parent anchors.margins: Theme.panelPadding spacing: 6 Text { text: "CONNECTION STATUS" color: Theme.sideMutedText font.pixelSize: Theme.fontSm font.bold: true font.letterSpacing: 1.5 font.family: Theme.uiFontFamily } Item { Layout.fillHeight: true } RowLayout { Layout.fillWidth: true spacing: 8 Item { Layout.fillWidth: true implicitHeight: 22 Row { anchors.centerIn: parent spacing: 5 Repeater { model: 4 Rectangle { width: 4; height: 4; radius: 2 anchors.verticalCenter: parent.verticalCenter color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText) opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3 SequentialAnimation on opacity { running: root.isConnected || root.isBusy loops: Animation.Infinite NumberAnimation { to: 0.3; duration: 1400 } NumberAnimation { to: 1.0; duration: 1400 } } } } Rectangle { anchors.verticalCenter: parent.verticalCenter width: statusPillText.implicitWidth + 16 height: 20 radius: 10 color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor) Text { id: statusPillText anchors.centerIn: parent text: root.isConnected ? "ACTIVE" : (root.isBusy ? deviceController.connectionStatus.replace("...", "").toUpperCase() : "INACTIVE") color: Theme.statusBadgeText font.pixelSize: Theme.fontXs font.weight: Font.Bold font.family: Theme.uiFontFamily font.letterSpacing: 0.6 } } Repeater { model: 4 Rectangle { width: 4; height: 4; radius: 2 anchors.verticalCenter: parent.verticalCenter color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText) opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3 SequentialAnimation on opacity { running: root.isConnected || root.isBusy loops: Animation.Infinite NumberAnimation { to: 0.3; duration: 1400 } NumberAnimation { to: 1.0; duration: 1400 } } } } } } Text { text: root.portName color: root.isConnected ? Theme.statusSuccessColor : Theme.headingColor font.pixelSize: Theme.fontMd font.weight: Font.Bold font.family: Theme.uiFontFamily elide: Text.ElideMiddle } } } } // ── Wafer & Sensor Data: far right, spans both rows ── Rectangle { Layout.rowSpan: 2 Layout.fillWidth: true Layout.fillHeight: true color: Theme.cardBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusSm ColumnLayout { anchors.fill: parent anchors.margins: Theme.panelPadding spacing: 4 Text { text: "WAFER & SENSOR DATA" color: Theme.sideMutedText font.pixelSize: Theme.fontSm font.weight: Font.Bold font.family: Theme.uiFontFamily font.letterSpacing: 0.6 Layout.bottomMargin: 2 } GridLayout { Layout.fillWidth: true Layout.fillHeight: true columns: 2 rowSpacing: 6 columnSpacing: 16 GridCell { id: waferInfoFamilyCell label: "Family Code" value: { var info = deviceController.lastWaferInfo; return (info && info.length > 0 && info[0]) ? info[0] : "—"; } } GridCell { id: waferSerialCell label: "Serial" value: { var info = deviceController.lastWaferInfo; return (info && info.length > 1 && info[1]) ? info[1] : "—"; } } Rectangle { Layout.columnSpan: 2 Layout.fillWidth: true Layout.preferredHeight: 1 color: Theme.cardBorder opacity: 0.5 } GridCell { id: waferCyclesCell label: "Cycles Completed" value: { var info = deviceController.lastWaferInfo; return (info && info.length > 5 && info[5] !== undefined) ? String(info[5]) : "—"; } active: root.waferDetected } GridCell { id: waferSensorsCell label: "Sensors" value: { var info = deviceController.lastWaferInfo; return (info && info.length > 2 && info[2]) ? String(info[2]) : "—"; } } } } } // ── Directory: bottom-left cell ── Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: Theme.cardBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusSm ColumnLayout { anchors.fill: parent anchors.margins: Theme.panelPadding spacing: 6 RowLayout { Layout.fillWidth: true spacing: 8 Text { text: "DIRECTORY" color: Theme.sideMutedText font.pixelSize: Theme.fontSm font.bold: true font.letterSpacing: 1.5 font.family: Theme.uiFontFamily Layout.fillWidth: true } Rectangle { visible: root.csvPath !== "" implicitWidth: sessionSavedLabel.implicitWidth + 12 implicitHeight: 18 radius: 4 color: Theme.fieldBackground border.color: Theme.sideBorder border.width: 1 Text { id: sessionSavedLabel anchors.centerIn: parent text: "SESSION SAVED" color: Theme.sideMutedText font.pixelSize: Theme.fontXs font.weight: Font.Bold font.family: Theme.uiFontFamily font.letterSpacing: 0.6 } } } Item { Layout.fillHeight: true } RowLayout { Layout.fillWidth: true spacing: 12 // Path pinned bottom-left Text { text: deviceController.saveDataDir || "Not configured" color: Theme.bodyColor font.pixelSize: Theme.fontMd font.family: Theme.uiFontFamily font.italic: true opacity: 0.7 elide: Text.ElideMiddle Layout.fillWidth: true } Button { id: browseBtn text: "Select Folder" font.pixelSize: Theme.fontSm font.weight: Font.Medium font.family: Theme.uiFontFamily implicitHeight: 24 hoverEnabled: true background: Rectangle { color: browseBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusSm } contentItem: Text { text: browseBtn.text color: Theme.bodyColor font: browseBtn.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } onClicked: dirOnlyDialog.open() } } } } // ── Total Runtime: bottom center ── StatCard { id: runtimeCard label: "TOTAL RUNTIME" value: { var info = deviceController.lastWaferInfo; if (!(info && info.length > 4 && info[4] !== undefined)) return "—"; var secs = parseFloat(info[4]); if (isNaN(secs)) return info[4] + "s"; var m = Math.floor(secs / 60); var s = Math.round(secs % 60); return secs + "s / " + m + ":" + (s < 10 ? "0" : "") + s + " min"; } } } // ═══════════════════════════════════════════════════════════════════════ // ROW 3: Activity Log — ALWAYS visible (fills remaining height) // ═══════════════════════════════════════════════════════════════════════ Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: Theme.cardBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusMd clip: true ColumnLayout { anchors.fill: parent spacing: 0 Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 color: Theme.subtleSectionBackground radius: Theme.radiusMd // Cover bottom corners so only top-left and top-right are rounded Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right height: parent.radius color: parent.color } RowLayout { anchors.fill: parent anchors.leftMargin: Theme.panelPadding anchors.rightMargin: Theme.panelPadding spacing: 8 IconImage { source: "icons/file-text.svg" width: 14; height: 14 sourceSize.width: 14 sourceSize.height: 14 color: Theme.sideMutedText visible: root.csvPath !== "" Layout.alignment: Qt.AlignVCenter } Label { text: (root.csvPath !== "" ? root.csvPath.split("/").pop() : "ACTIVITY LOG").toUpperCase() color: Theme.sideMutedText font.pixelSize: Theme.fontMd font.letterSpacing: 1.5 font.bold: true font.family: Theme.uiFontFamily elide: Text.ElideMiddle Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter } } Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right height: 1 color: Theme.cardBorder } } ScrollView { Layout.fillWidth: true Layout.fillHeight: true Layout.margins: Theme.panelPadding clip: true TextArea { id: activityLog width: parent.width // Bound to backend so log survives tab switches (Loader // destroys this tab); clears only via Clear Log / restart. text: deviceController.activityLog || qsTr("Welcome — use the side rail to detect a wafer, or import data to review.") readOnly: true font.family: "monospace" font.pixelSize: Theme.fontSm wrapMode: TextArea.WordWrap color: Theme.bodyColor background: null } } } } // ═══════════════════════════════════════════════════════════════════════ // SIGNAL HANDLERS // ═══════════════════════════════════════════════════════════════════════ Connections { target: deviceController function onLogMessage(message) { if (message == "CHOOSE_SAVE_DIR") { saveDirDialog.open(); } } function onPortsUpdated() {} function onDetectResult(result) { if (!result) { root.dataParsed = false; root.dataRows = 0; root.dataCols = 0; root.csvPath = ""; } } function onStatusRestored() { if (deviceController.dataRowCount > 0) { root.dataParsed = true; root.dataRows = deviceController.dataRowCount; root.dataCols = deviceController.dataColCount; root.csvPath = ""; } } } Connections { target: deviceController function onReadResult(result) { root.dataParsed = false; root.dataRows = 0; root.dataCols = 0; root.csvPath = ""; if (result && result.success === true) saveDirDialog.open(); } } Connections { target: deviceController function onParsedDataReady(result) { if (result && result.success) { root.dataParsed = true; root.dataRows = result.rows; root.dataCols = result.cols; root.csvPath = result.csv_path || ""; if (root.csvPath) { metaWafer.text = result.serialNumber || ""; metaDate.text = Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss"); metaChamber.text = settingsModel.chamberId || ""; metaNotes.text = ""; editCsvDialog.open(); } } else { root.dataParsed = false; } } } }