feat(license): runtime trial gating UI + appendActivityLog entry point
This commit is contained in:
@@ -33,6 +33,9 @@ Rectangle {
|
|||||||
property string dataCompareFileB: ""
|
property string dataCompareFileB: ""
|
||||||
|
|
||||||
onSelectedTabIndexChanged: {
|
onSelectedTabIndexChanged: {
|
||||||
|
// Time-based, so re-check on every switch rather than trusting a
|
||||||
|
// value computed at some earlier point in a possibly long-running session.
|
||||||
|
root.reviewBlocked = licenseModel.reviewAccessBlocked()
|
||||||
if (selectedTabIndex === 2) {
|
if (selectedTabIndex === 2) {
|
||||||
file_browser.refreshFiles();
|
file_browser.refreshFiles();
|
||||||
if (root.mapReviewFile !== "") {
|
if (root.mapReviewFile !== "") {
|
||||||
@@ -68,6 +71,9 @@ Rectangle {
|
|||||||
if (root.selectedTabIndex === 2) root.mapReviewFile = streamController.loadedFile
|
if (root.selectedTabIndex === 2) root.mapReviewFile = streamController.loadedFile
|
||||||
else if (root.selectedTabIndex === 3) root.graphReviewFile = streamController.loadedFile
|
else if (root.selectedTabIndex === 3) root.graphReviewFile = streamController.loadedFile
|
||||||
}
|
}
|
||||||
|
function onLiveError(message) {
|
||||||
|
deviceController.appendActivityLog("Live stream error — stream stopped: " + message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data tab's Run A/B picks survive it being destroyed/recreated by its
|
// Data tab's Run A/B picks survive it being destroyed/recreated by its
|
||||||
@@ -84,6 +90,24 @@ Rectangle {
|
|||||||
property bool waferDetected: false
|
property bool waferDetected: false
|
||||||
property bool waferLicensed: false
|
property bool waferLicensed: false
|
||||||
|
|
||||||
|
// Data/Graph/Map-review are gated on the 30-day trial once it's actually
|
||||||
|
// expired (ADR-0005 "Deferred" item — the trigger existed, nothing ever
|
||||||
|
// checked it). Status stays free: hardware actions are gated separately,
|
||||||
|
// per-wafer-serial, via waferLicensed above.
|
||||||
|
property bool reviewBlocked: false
|
||||||
|
onReviewBlockedChanged: {
|
||||||
|
// Property change signals only fire on an actual value change, so
|
||||||
|
// this logs once per none/temporary → expired transition, not on
|
||||||
|
// every recompute in onSelectedTabIndexChanged/onLicensesChanged.
|
||||||
|
if (reviewBlocked) {
|
||||||
|
deviceController.appendActivityLog(
|
||||||
|
"Review trial expired — load a wafer license to continue using Data/Map/Graph (About dialog)")
|
||||||
|
if (root.selectedTabIndex !== 0) {
|
||||||
|
root.selectedTabIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: deviceController
|
target: deviceController
|
||||||
function onDetectResult(result){
|
function onDetectResult(result){
|
||||||
@@ -124,6 +148,7 @@ Rectangle {
|
|||||||
root.selectedTabIndex = 0
|
root.selectedTabIndex = 0
|
||||||
}
|
}
|
||||||
root.waferLicensed = root.waferDetected && deviceController.waferLicensed()
|
root.waferLicensed = root.waferDetected && deviceController.waferLicensed()
|
||||||
|
root.reviewBlocked = licenseModel.reviewAccessBlocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
@@ -133,9 +158,9 @@ Rectangle {
|
|||||||
result !== undefined &&
|
result !== undefined &&
|
||||||
result.success === true)
|
result.success === true)
|
||||||
if (root.memoryRead) {
|
if (root.memoryRead) {
|
||||||
console.log("[P1.1] Memory read complete:", result.bytes, "bytes")
|
console.log("Memory read complete:", result.bytes, "bytes")
|
||||||
} else {
|
} else {
|
||||||
console.log("[P1.1] Read failed:", result.error || "unknown")
|
console.log("Read failed:", result.error || "unknown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,6 +296,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
root.reviewBlocked = licenseModel.reviewAccessBlocked()
|
||||||
if (isFirstLaunch) {
|
if (isFirstLaunch) {
|
||||||
firstLaunchWizard.open()
|
firstLaunchWizard.open()
|
||||||
}
|
}
|
||||||
@@ -342,10 +368,13 @@ Rectangle {
|
|||||||
id: pillBtn
|
id: pillBtn
|
||||||
checked: root.selectedTabIndex === index
|
checked: root.selectedTabIndex === index
|
||||||
// MAP (index 2) is locked until a valid license .bin is installed.
|
// MAP (index 2) is locked until a valid license .bin is installed.
|
||||||
|
// DATA/MAP/GRAPH (index !== 0) also lock once the 30-day review
|
||||||
|
// trial has actually expired with no permanent license (ADR-0005).
|
||||||
// Stays enabled (not `enabled: false`) so hover still fires and the
|
// Stays enabled (not `enabled: false`) so hover still fires and the
|
||||||
// tooltip can explain why — a disabled Item receives no mouse/hover
|
// tooltip can explain why — a disabled Item receives no mouse/hover
|
||||||
// events at all, which would silently kill the tooltip below.
|
// events at all, which would silently kill the tooltip below.
|
||||||
property bool locked: index === 2 && licenseModel.licenses.length === 0
|
property bool locked: (index === 2 && licenseModel.licenses.length === 0)
|
||||||
|
|| (index !== 0 && root.reviewBlocked)
|
||||||
flat: true
|
flat: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
@@ -411,7 +440,13 @@ Rectangle {
|
|||||||
|
|
||||||
AppToolTip {
|
AppToolTip {
|
||||||
visible: pillBtn.hovered
|
visible: pillBtn.hovered
|
||||||
text: (index === 2 && licenseModel.licenses.length === 0)
|
// Trial-expired is checked first: it's the broader reason and
|
||||||
|
// applies identically to Data/Map/Graph, so when it's true it
|
||||||
|
// wins even for MAP — otherwise MAP's narrower "no license at
|
||||||
|
// all" case (trial not yet expired, still just unlicensed) shows.
|
||||||
|
text: (index !== 0 && root.reviewBlocked)
|
||||||
|
? "<b>" + model.label + "</b>: Review trial expired — load a license in About to unlock"
|
||||||
|
: (index === 2 && licenseModel.licenses.length === 0)
|
||||||
? "<b>MAP</b>: No license loaded — load one in About to unlock"
|
? "<b>MAP</b>: No license loaded — load one in About to unlock"
|
||||||
: "<b>" + model.label + "</b>: " + model.desc
|
: "<b>" + model.label + "</b>: " + model.desc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,13 @@ class DeviceController(QObject):
|
|||||||
self._activity_log = self._activity_log[-200:]
|
self._activity_log = self._activity_log[-200:]
|
||||||
self.activityLogUpdated.emit("\n".join(self._activity_log))
|
self.activityLogUpdated.emit("\n".join(self._activity_log))
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
@slot_error_boundary
|
||||||
|
def appendActivityLog(self, message: str) -> None:
|
||||||
|
"""Public entry point for QML-side conditions (e.g. license/trial
|
||||||
|
state) to log into the same Activity Log as backend-triggered lines."""
|
||||||
|
self._append_log(message)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
@slot_error_boundary
|
@slot_error_boundary
|
||||||
def clearActivityLog(self) -> None:
|
def clearActivityLog(self) -> None:
|
||||||
|
|||||||
@@ -267,3 +267,33 @@ def test_replay_trial_expiry(tmp_path, qapp):
|
|||||||
def test_replay_trial_expired_without_marker(tmp_path, qapp):
|
def test_replay_trial_expired_without_marker(tmp_path, qapp):
|
||||||
model = LicenseModel(str(tmp_path))
|
model = LicenseModel(str(tmp_path))
|
||||||
assert model.replayTrialExpired() is True
|
assert model.replayTrialExpired() is True
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Review-access gate (ADR-0005 "Deferred" item) =====
|
||||||
|
def test_review_access_not_blocked_before_trial_starts(tmp_path, qapp):
|
||||||
|
model = LicenseModel(str(tmp_path))
|
||||||
|
assert model.reviewAccessBlocked() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_access_not_blocked_during_fresh_trial(tmp_path, qapp):
|
||||||
|
model = LicenseModel(str(tmp_path))
|
||||||
|
model.startReplayTrial()
|
||||||
|
assert model.reviewAccessBlocked() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_access_blocked_once_trial_expired(tmp_path, qapp):
|
||||||
|
model = LicenseModel(str(tmp_path))
|
||||||
|
model.startReplayTrial()
|
||||||
|
old = time.time() - 31 * 86400
|
||||||
|
os.utime(model._trial_marker, (old, old))
|
||||||
|
assert model.reviewAccessBlocked() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_access_not_blocked_with_permanent_license_even_if_trial_expired(tmp_path, qapp):
|
||||||
|
(tmp_path / "licenses").mkdir()
|
||||||
|
write_license(tmp_path / "licenses", level="01")
|
||||||
|
model = LicenseModel(str(tmp_path))
|
||||||
|
model.startReplayTrial()
|
||||||
|
old = time.time() - 31 * 86400
|
||||||
|
os.utime(model._trial_marker, (old, old))
|
||||||
|
assert model.reviewAccessBlocked() is False
|
||||||
|
|||||||
Reference in New Issue
Block a user