From 115f0e519d5ebd0145131568ec889d05860b7cab Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:02:34 +0700 Subject: [PATCH 1/9] fix(layout): bounded compact injector column, flexible engineering plots at desktop and maximized sizes --- apps/arstack_studio/qml/Main.qml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index e0ee2e98c..126652474 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1065,8 +1065,12 @@ ApplicationWindow { objectName: "studioInjectionSurface" SplitView.fillWidth: true SplitView.fillHeight: true - SplitView.minimumWidth: root.compactLayout ? 500 : 550 - SplitView.preferredWidth: root.compactLayout ? 700 : 860 + // Form fields must not expand across the whole screen. + // Keep the injector compact; give spare width to plots. + // Users can still resize the split manually. + SplitView.minimumWidth: root.compactLayout ? 480 : 520 + SplitView.preferredWidth: root.compactLayout ? 505 : 610 + SplitView.maximumWidth: 660 theme: studioTheme ColumnLayout { @@ -1295,9 +1299,9 @@ ApplicationWindow { SplitView { visible: (root.phasorDockVisible && !root.phasorDetached) || (root.waveformDockVisible && !root.waveformDetached) - SplitView.preferredWidth: root.compactLayout ? 300 : 380 - SplitView.minimumWidth: root.compactLayout ? 270 : 300 - SplitView.maximumWidth: 680 + SplitView.preferredWidth: root.compactLayout ? 420 : 560 + SplitView.minimumWidth: root.compactLayout ? 330 : 380 + SplitView.maximumWidth: 1200 SplitView.fillHeight: true orientation: Qt.Vertical handle: Rectangle { From 29f614d494394014b531788b89e91b65a1dd6791 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:02:59 +0700 Subject: [PATCH 2/9] feat(icons): add user-requested run-bolt state glyph --- apps/arstack_studio/assets/material/run-bolt.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/arstack_studio/assets/material/run-bolt.svg diff --git a/apps/arstack_studio/assets/material/run-bolt.svg b/apps/arstack_studio/assets/material/run-bolt.svg new file mode 100644 index 000000000..6df727593 --- /dev/null +++ b/apps/arstack_studio/assets/material/run-bolt.svg @@ -0,0 +1 @@ + From 3e97e542e2d60964dc746e05bb5f6a07857c2ce0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:03:05 +0700 Subject: [PATCH 3/9] feat(icons): add user-requested stop-bolt state glyph --- apps/arstack_studio/assets/material/stop-bolt.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/arstack_studio/assets/material/stop-bolt.svg diff --git a/apps/arstack_studio/assets/material/stop-bolt.svg b/apps/arstack_studio/assets/material/stop-bolt.svg new file mode 100644 index 000000000..71035e290 --- /dev/null +++ b/apps/arstack_studio/assets/material/stop-bolt.svg @@ -0,0 +1 @@ + From a313abaf840553597fcd563fa2683dc19f2f14e2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:03:33 +0700 Subject: [PATCH 4/9] feat(ribbon): use engineer-supplied bolt glyph RUN green / STOP muted red based only on board state --- apps/arstack_studio/qml/ModernRibbon.qml | 36 ++++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/arstack_studio/qml/ModernRibbon.qml b/apps/arstack_studio/qml/ModernRibbon.qml index 193af1705..1f913ab45 100644 --- a/apps/arstack_studio/qml/ModernRibbon.qml +++ b/apps/arstack_studio/qml/ModernRibbon.qml @@ -220,11 +220,13 @@ Rectangle { } Rectangle { - Layout.preferredWidth: bar.compact ? 84 : 108 - Layout.preferredHeight: 36 + Layout.preferredWidth: bar.compact ? 92 : 120 + Layout.preferredHeight: 39 Layout.alignment: Qt.AlignVCenter - radius: 15 - color: "#0a1016" + radius: 12 + // A noninteractive status, not another START/STOP authority. + // Green only after verified board RUNNING; muted red otherwise. + color: bar.device.running ? "#112a23" : "#271a20" border.width: 1 border.color: Qt.rgba(bar.workflow.smartStateColor.r, bar.workflow.smartStateColor.g, @@ -236,21 +238,25 @@ Rectangle { anchors.rightMargin: 9 spacing: 6 - Rectangle { - width: 6 - height: 6 - radius: 3 - color: bar.workflow.smartStateColor + Image { + source: bar.device.running + ? Qt.resolvedUrl("../assets/material/run-bolt.svg") + : Qt.resolvedUrl("../assets/material/stop-bolt.svg") + sourceSize.width: 21 + sourceSize.height: 21 + Layout.preferredWidth: 21 + Layout.preferredHeight: 21 + fillMode: Image.PreserveAspectFit + opacity: bar.device.running ? 1.0 : 0.72 } - Label { Layout.fillWidth: true - text: bar.conciseState() - color: bar.workflow.smartStateColor + text: bar.device.running ? "RUN" : "STOP" + color: bar.device.running ? bar.theme.green : "#b45d67" font.family: bar.uiFont - font.pixelSize: 9 - font.weight: Font.DemiBold - elide: Text.ElideRight + font.pixelSize: 12 + font.weight: Font.Bold + font.letterSpacing: 0.7 verticalAlignment: Text.AlignVCenter } } From f722f519d7532f453110f7ead250890eaa82e4ff Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:04:27 +0700 Subject: [PATCH 5/9] fix(start): retain first operator click only through same-boot 4I+4V preparation; bounded cancellable handoff --- apps/arstack_studio/qml/WorkflowBar.qml | 77 +++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index 06f5d88c6..f28210e89 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -17,14 +17,64 @@ SurfacePanel { property bool compact: false // Firmware transaction is the single source of truth. UI timers and // unrelated PTP/serial errors must not bounce the injection START button. - readonly property bool startQueued: Boolean(ribbon.device && ribbon.device.scenarioApplying) + readonly property bool startQueued: Boolean( + (ribbon.device && ribbon.device.scenarioApplying) || ribbon.startIntentWaiting) + // One deliberate click while 4I+4V is preparing remains armed only for + // this verified board boot. No automatic START after USB reconnect/flash. + property bool startIntentWaiting: false + property string startIntentBootId: "" + function cancelStartIntent() { + startIntentWaiting = false + startIntentBootId = "" + startIntentDeadline.stop() + } + Timer { + id: startIntentDeadline + interval: 8000 + repeat: false + onTriggered: { + if (!ribbon.startIntentWaiting) return + ribbon.cancelStartIntent() + controller.showMessage( + "START belum dikirim: persiapan profil 4I+4V melebihi 8 detik. Periksa status board dan coba lagi.", true) + } + } + function deliverReadyStart() { + if (!startIntentWaiting || !ribbon.device || !ribbon.device.deviceVerified) return + if (ribbon.device.bootId !== startIntentBootId || + smartSession.updatingFirmware || smartSession.updateNeedsBootloaderHelp) { + cancelStartIntent() + return + } + if (ribbon.device.running || ribbon.device.scenarioApplying) { + cancelStartIntent() + return + } + if (!smartSession.startReady) return + cancelStartIntent() + // Snapshot is taken at the actual verified start boundary, not from + // stale startup defaults. No hidden retries after a rejected command. + const snapshot = controller.startScenarioSnapshot() + if (!snapshot) return + if (!smartSession.requestStartWithScenario(snapshot)) { + controller.pendingApply = true + controller.showMessage("START ditolak board. Periksa status koneksi dan diagnostics.", true) + } + } Connections { target: ribbon.device function onRunningChanged() { if (ribbon.device.running) controller.pendingApply = false } function onDeviceVerifiedChanged() { - if (!ribbon.device.deviceVerified) controller.pendingApply = true + if (!ribbon.device.deviceVerified) { + controller.pendingApply = true + ribbon.cancelStartIntent() + } + } + function onDeviceIdentityChanged() { + if (ribbon.startIntentWaiting && ribbon.device.bootId !== ribbon.startIntentBootId) + ribbon.cancelStartIntent() } } property bool updatePromptDeferred: false @@ -123,6 +173,7 @@ SurfacePanel { Connections { target: smartSession function onReadyForLiveApply() { + if (ribbon.startIntentWaiting) Qt.callLater(ribbon.deliverReadyStart) // The current board SHOW response does not contain per-channel // amplitude/phase/quality. Never silently claim readback or // overwrite a board with a restored/offline local draft. @@ -132,6 +183,7 @@ SurfacePanel { // No auto-transmit on device discovery/reconnection. } function onStateChanged() { + if (ribbon.startIntentWaiting) Qt.callLater(ribbon.deliverReadyStart) if (ribbon.firmwareGateActive) controller.profileDirty = true @@ -271,9 +323,17 @@ SurfacePanel { return } if (!smartSession.startReady) { - // One click must explain why a physical START is not queued. - // Never make the operator click twice to discover readiness. - controller.showMessage("START waiting: " + ribbon.startReason(), true) + if (ribbon.device.deviceVerified && + smartSession.state === "PREPARING 4I+4V" && + ribbon.device.bootId.length > 0) { + ribbon.startIntentBootId = ribbon.device.bootId + ribbon.startIntentWaiting = true + startIntentDeadline.restart() + controller.showMessage( + "START diterima. Menunggu profil 4I+4V siap; injeksi dimulai setelah board terverifikasi.", false) + } else { + controller.showMessage("START belum siap: " + ribbon.startReason(), true) + } return } // A START always transmits the current 4I+4V table snapshot. @@ -291,6 +351,13 @@ SurfacePanel { } function requestStop() { + // An operator STOP cancels a queued one-click intent before any + // firmware command can be dispatched. + if (startIntentWaiting) { + cancelStartIntent() + controller.showMessage("START dibatalkan. Board tetap STOP.", false) + return + } // A pending CFG transaction can progress to START: Stop must remain // actionable before RUNNING telemetry, not only after it. if (!ribbon.injectionRunning && !ribbon.startQueued) return From 5a7605452663ac1d2b07f9fb2237501a83161a5a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:04:52 +0700 Subject: [PATCH 6/9] test(ui): assert compact proportional workspace, verified bolt status, bounded first-click START intent --- .../tests/check_operator_workspace.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index fe344ee67..a06e31c86 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -537,6 +537,27 @@ def main() -> None: "SV output must fail-closed on measured heartbeat loss, preserve board STOP cause, " "and never show misleading green READY after an unrequested stop") + # Operator desktop layout: bounded injector column, space for plots, + # no extra QML START authority and no abandoned first-click intent. + layout = source("Main.qml") + ribbon = source("ModernRibbon.qml") + workflow = source("WorkflowBar.qml") + require("SplitView.maximumWidth: 660" in layout + and "SplitView.preferredWidth: root.compactLayout ? 505 : 610" in layout + and "SplitView.preferredWidth: root.compactLayout ? 420 : 560" in layout + and "run-bolt.svg" in ribbon and "stop-bolt.svg" in ribbon + and 'text: bar.device.running ? "RUN" : "STOP"' in ribbon + and "property bool startIntentWaiting: false" in workflow + and "interval: 8000" in workflow + and "startIntentBootId" in workflow + and "function cancelStartIntent()" in workflow + and "function deliverReadyStart()" in workflow + and "smartSession.state === \"PREPARING 4I+4V\"" in workflow + and "if (startIntentWaiting)" in workflow + and "smartSession.requestStartWithScenario(snapshot)" in workflow, + "Studio must offer proportional engineering panels, verified bolt RUN/STOP " + "and first-click START through a bounded same-boot cancellable intent") + toast_layout = source("Main.qml") toast_view = source("StatusToast.qml") require("function explainFailure(message)" in toast_layout From 0a6dad7413f02319dd86a3f477c4ba1dfc99cde4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:07:35 +0700 Subject: [PATCH 7/9] test(studio): retain STOP priority contract with bounded first-click START staging --- apps/arstack_studio/tests/check_operator_workspace.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index a06e31c86..839f8e530 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -191,7 +191,10 @@ def main() -> None: require('bar.workflow.injectionRunning || bar.workflow.startQueued || bar.controller.offlinePreviewRunning' in ribbon_qml and '!ribbon.injectionRunning && !ribbon.startQueued' in source("WorkflowBar.qml") - and 'readonly property bool startQueued: Boolean(ribbon.device && ribbon.device.scenarioApplying)' in source("WorkflowBar.qml") + and 'readonly property bool startQueued: Boolean(' in source("WorkflowBar.qml") + and 'ribbon.device.scenarioApplying) || ribbon.startIntentWaiting' in source("WorkflowBar.qml") + and 'if (startIntentWaiting)' in source("WorkflowBar.qml") + and 'cancelStartIntent()' in source("WorkflowBar.qml") and 'startWatchdog' not in source("WorkflowBar.qml"), "SV STOP must remain actionable while the acknowledged scenario transaction is pending") require("function flushPendingEdits()" in matrix From 7789168c1b668a73fb0c0b37e14652ed3e463ce4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:17:18 +0700 Subject: [PATCH 8/9] fix(packaging): embed verified RUN/STOP bolt SVG resources in Qt Windows installer --- apps/arstack_studio/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/arstack_studio/CMakeLists.txt b/apps/arstack_studio/CMakeLists.txt index 993df310e..47bb3f659 100644 --- a/apps/arstack_studio/CMakeLists.txt +++ b/apps/arstack_studio/CMakeLists.txt @@ -158,6 +158,8 @@ qt_add_qml_module(arstack_studio assets/material/file_upload.svg assets/material/stop.svg assets/material/play_arrow.svg + assets/material/run-bolt.svg + assets/material/stop-bolt.svg assets/material/dashboard.svg assets/material/folder_open.svg assets/material/search.svg From bb4b02f1fa3580e30297d9bbb4e049d3b23b5073 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 14:17:35 +0700 Subject: [PATCH 9/9] test(packaging): prevent missing RUN/STOP icons in deployed QML resources --- apps/arstack_studio/tests/check_operator_workspace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 839f8e530..a80c31b31 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -549,6 +549,8 @@ def main() -> None: and "SplitView.preferredWidth: root.compactLayout ? 505 : 610" in layout and "SplitView.preferredWidth: root.compactLayout ? 420 : 560" in layout and "run-bolt.svg" in ribbon and "stop-bolt.svg" in ribbon + and "assets/material/run-bolt.svg" in (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") + and "assets/material/stop-bolt.svg" in (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") and 'text: bar.device.running ? "RUN" : "STOP"' in ribbon and "property bool startIntentWaiting: false" in workflow and "interval: 8000" in workflow