feat(license): runtime trial gating UI + appendActivityLog entry point
This commit is contained in:
@@ -33,6 +33,9 @@ Rectangle {
|
||||
property string dataCompareFileB: ""
|
||||
|
||||
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) {
|
||||
file_browser.refreshFiles();
|
||||
if (root.mapReviewFile !== "") {
|
||||
@@ -68,6 +71,9 @@ Rectangle {
|
||||
if (root.selectedTabIndex === 2) root.mapReviewFile = 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
|
||||
@@ -84,6 +90,24 @@ Rectangle {
|
||||
property bool waferDetected: 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 {
|
||||
target: deviceController
|
||||
function onDetectResult(result){
|
||||
@@ -124,6 +148,7 @@ Rectangle {
|
||||
root.selectedTabIndex = 0
|
||||
}
|
||||
root.waferLicensed = root.waferDetected && deviceController.waferLicensed()
|
||||
root.reviewBlocked = licenseModel.reviewAccessBlocked()
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
@@ -133,9 +158,9 @@ Rectangle {
|
||||
result !== undefined &&
|
||||
result.success === true)
|
||||
if (root.memoryRead) {
|
||||
console.log("[P1.1] Memory read complete:", result.bytes, "bytes")
|
||||
console.log("Memory read complete:", result.bytes, "bytes")
|
||||
} else {
|
||||
console.log("[P1.1] Read failed:", result.error || "unknown")
|
||||
console.log("Read failed:", result.error || "unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +296,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.reviewBlocked = licenseModel.reviewAccessBlocked()
|
||||
if (isFirstLaunch) {
|
||||
firstLaunchWizard.open()
|
||||
}
|
||||
@@ -342,10 +368,13 @@ Rectangle {
|
||||
id: pillBtn
|
||||
checked: root.selectedTabIndex === index
|
||||
// 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
|
||||
// tooltip can explain why — a disabled Item receives no mouse/hover
|
||||
// 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
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
@@ -411,9 +440,15 @@ Rectangle {
|
||||
|
||||
AppToolTip {
|
||||
visible: pillBtn.hovered
|
||||
text: (index === 2 && licenseModel.licenses.length === 0)
|
||||
? "<b>MAP</b>: No license loaded — load one in About to unlock"
|
||||
: "<b>" + model.label + "</b>: " + model.desc
|
||||
// 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>" + model.label + "</b>: " + model.desc
|
||||
}
|
||||
|
||||
onClicked: if (!pillBtn.locked) root.selectedTabIndex = index
|
||||
|
||||
@@ -303,6 +303,13 @@ class DeviceController(QObject):
|
||||
self._activity_log = self._activity_log[-200:]
|
||||
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_error_boundary
|
||||
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):
|
||||
model = LicenseModel(str(tmp_path))
|
||||
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