polkit: add temporary FPC confirmation
This commit is contained in:
parent
902a724855
commit
658217e72e
7 changed files with 374 additions and 110 deletions
|
|
@ -63,6 +63,7 @@ modules/souveraine/windowSheet/PowerMenu.qml souveraine/modules/souveraine/windo
|
|||
modules/souveraine/windowSheet/PowerOptionRow.qml souveraine/modules/souveraine/windowSheet/PowerOptionRow.qml
|
||||
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
|
||||
services/Face.qml souveraine/services/Face.qml
|
||||
services/FingerprintPreview.qml souveraine/services/FingerprintPreview.qml
|
||||
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
|
||||
services/ZoneTransition.qml souveraine/services/ZoneTransition.qml
|
||||
services/Hyprsunset.qml souveraine/services/Hyprsunset.qml
|
||||
|
|
@ -114,6 +115,7 @@ modules/settings/KeyboardConfig.qml souveraine/modules/settings/KeyboardConfig.q
|
|||
modules/settings/IdleConfig.qml souveraine/modules/settings/IdleConfig.qml
|
||||
modules/settings/SpeechConfig.qml souveraine/modules/settings/SpeechConfig.qml
|
||||
modules/ii/polkit/Polkit.qml souveraine/modules/ii/polkit/Polkit.qml
|
||||
modules/ii/polkit/PolkitContent.qml souveraine/modules/ii/polkit/PolkitContent.qml
|
||||
modules/ii/dock/Dock.qml souveraine/modules/ii/dock/Dock.qml
|
||||
modules/ii/dock/DockManifest.qml souveraine/modules/ii/dock/DockManifest.qml
|
||||
modules/ii/dock/DockApps.qml souveraine/modules/ii/dock/DockApps.qml
|
||||
|
|
|
|||
|
|
@ -481,13 +481,20 @@ Singleton {
|
|||
// (background.wallpaperPath); a path pins the lock's own.
|
||||
property string wallpaperPath: ""
|
||||
property bool showLockedText: true
|
||||
// A visible wiring exercise for the FPC1020 path. It is not
|
||||
// biometric authentication: it may acknowledge a sensor pulse
|
||||
// and a local hold, but it never unlocks or mints a grant.
|
||||
// A visible wiring exercise for the FPC1020 path. It never
|
||||
// unlocks the session; the separate Polkit setting below is
|
||||
// the temporary, post-login-only factor.
|
||||
property JsonObject fingerprintPreview: JsonObject {
|
||||
property bool enabled: false
|
||||
property int holdMs: 3000
|
||||
}
|
||||
// The temporary FPC factor belongs only to polkit-1. It cannot
|
||||
// satisfy first login, disk unlock, sudo, SSH, or lock-screen
|
||||
// PAM. The real souveraine-fpd match result will replace this
|
||||
// raw-reader bridge without changing the user-facing surface.
|
||||
property JsonObject fingerprintPolkit: JsonObject {
|
||||
property bool enabled: true
|
||||
}
|
||||
// Souveraine-owned lock cards. Transport controls are
|
||||
// ambient; media metadata remains personal by default.
|
||||
property JsonObject content: JsonObject {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
|
@ -23,24 +24,20 @@ Scope {
|
|||
property var targetAction: LockContext.ActionEnum.Unlock
|
||||
property bool alsoInhibitIdle: false
|
||||
|
||||
// This is deliberately an integration preview, not a biometric factor.
|
||||
// The FPC1020's KEY_WAKEUP edge proves only that something asserted the
|
||||
// line; it does not prove a matched enrolled fingerprint. Until the
|
||||
// attested souveraine-fpd path exists, neither a pulse nor a touch hold
|
||||
// may reach unlocked() or StepUpAuth.
|
||||
// FingerprintPreview owns the FPC pulse and hold state for both lock and
|
||||
// step-up. This context only exposes it to the lock surface; it never
|
||||
// decides whether a hold unlocks the session.
|
||||
readonly property bool provisionalFingerprintEnabled:
|
||||
Config.options?.lock?.fingerprintPreview?.enabled ?? false
|
||||
FingerprintPreview.previewEnabled
|
||||
readonly property int provisionalFingerprintHoldMs:
|
||||
Config.options?.lock?.fingerprintPreview?.holdMs ?? 3000
|
||||
property bool provisionalFingerprintHolding: false
|
||||
property bool provisionalFingerprintConfirmed: false
|
||||
property bool provisionalFingerprintPulseSeen: false
|
||||
property real provisionalFingerprintHoldProgress: 0
|
||||
property double provisionalFingerprintHoldStartedAt: 0
|
||||
property string provisionalFingerprintPulseToken: ""
|
||||
|
||||
signal provisionalFingerprintPulse()
|
||||
signal provisionalFingerprintPreviewConfirmed()
|
||||
FingerprintPreview.holdMs
|
||||
readonly property bool provisionalFingerprintHolding:
|
||||
FingerprintPreview.holding && FingerprintPreview.activePurpose === "lock"
|
||||
readonly property bool provisionalFingerprintConfirmed:
|
||||
FingerprintPreview.confirmed && FingerprintPreview.confirmedPurpose === "lock"
|
||||
readonly property bool provisionalFingerprintPulseSeen: FingerprintPreview.pulseSeen
|
||||
readonly property real provisionalFingerprintHoldProgress:
|
||||
FingerprintPreview.activePurpose === "lock" ? FingerprintPreview.holdProgress : 0
|
||||
|
||||
function resetTargetAction() {
|
||||
root.targetAction = LockContext.ActionEnum.Unlock;
|
||||
|
|
@ -63,107 +60,25 @@ Scope {
|
|||
}
|
||||
|
||||
function beginProvisionalFingerprintHold() {
|
||||
if (!root.provisionalFingerprintEnabled) return;
|
||||
root.provisionalFingerprintConfirmed = false;
|
||||
root.provisionalFingerprintHolding = true;
|
||||
root.provisionalFingerprintHoldStartedAt = Date.now();
|
||||
root.provisionalFingerprintHoldProgress = 0;
|
||||
provisionalFingerprintHoldTimer.start();
|
||||
FingerprintPreview.beginHold("lock");
|
||||
}
|
||||
|
||||
function cancelProvisionalFingerprintHold() {
|
||||
if (!root.provisionalFingerprintHolding) return;
|
||||
root.provisionalFingerprintHolding = false;
|
||||
root.provisionalFingerprintHoldProgress = 0;
|
||||
provisionalFingerprintHoldTimer.stop();
|
||||
FingerprintPreview.cancelHold("lock");
|
||||
}
|
||||
|
||||
function confirmProvisionalFingerprintHold() {
|
||||
if (!root.provisionalFingerprintHolding) return;
|
||||
root.provisionalFingerprintHolding = false;
|
||||
root.provisionalFingerprintHoldProgress = 1;
|
||||
provisionalFingerprintHoldTimer.stop();
|
||||
root.provisionalFingerprintConfirmed = true;
|
||||
root.provisionalFingerprintPreviewConfirmed();
|
||||
provisionalFingerprintConfirmTimer.restart();
|
||||
console.log("[fingerprint-preview] hold confirmed; no authentication granted");
|
||||
FingerprintPreview.confirmHold("lock");
|
||||
}
|
||||
|
||||
// Called by the narrow future producer seam (`fingerprint.signal`). This
|
||||
// records only that an input path reached the surface. Any same-user IPC
|
||||
// caller can cause this today, so it is intentionally visual-only.
|
||||
// Called by the diagnostic `fingerprint.signal` IPC seam. It reaches the
|
||||
// same one-owner state as the root-owned FPC producer record.
|
||||
function noteProvisionalFingerprintPulse() {
|
||||
if (!root.provisionalFingerprintEnabled)
|
||||
return { ok: false, code: "not_enabled", reason: "fingerprint preview is disabled" };
|
||||
root.provisionalFingerprintPulseSeen = true;
|
||||
root.provisionalFingerprintPulse();
|
||||
provisionalFingerprintPulseTimer.restart();
|
||||
console.log("[fingerprint-preview] pulse observed; no authentication granted");
|
||||
return { ok: true, status: "observed" };
|
||||
return FingerprintPreview.notePulse();
|
||||
}
|
||||
|
||||
function resetProvisionalFingerprint() {
|
||||
root.provisionalFingerprintHolding = false;
|
||||
root.provisionalFingerprintConfirmed = false;
|
||||
root.provisionalFingerprintPulseSeen = false;
|
||||
root.provisionalFingerprintHoldProgress = 0;
|
||||
provisionalFingerprintHoldTimer.stop();
|
||||
provisionalFingerprintConfirmTimer.stop();
|
||||
provisionalFingerprintPulseTimer.stop();
|
||||
}
|
||||
|
||||
// blueline-fingerprintd is the only source that may write this root-owned
|
||||
// status file. Its record says an FPC IRQ reached the daemon; it is not a
|
||||
// match, and this listener has no authority beyond the preview pulse.
|
||||
FileView {
|
||||
id: provisionalFingerprintPulseFile
|
||||
path: "/run/blueline-fingerprintd/preview-pulse"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
const record = JSON.parse(provisionalFingerprintPulseFile.text());
|
||||
const token = `${record.sequence}:${record.at_ms}`;
|
||||
if (token === root.provisionalFingerprintPulseToken) return;
|
||||
root.provisionalFingerprintPulseToken = token;
|
||||
|
||||
// Do not replay an old event merely because the lock surface
|
||||
// appeared after it. A new reader assertion has a fresh wall
|
||||
// timestamp and reaches the same visual-only method as the IPC
|
||||
// diagnostic seam.
|
||||
const ageMs = Date.now() - Number(record.at_ms);
|
||||
if (Number(record.sequence) > 0 && ageMs >= 0 && ageMs < 5000)
|
||||
root.noteProvisionalFingerprintPulse();
|
||||
} catch (error) {
|
||||
console.warn("[fingerprint-preview] invalid FPC pulse record:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: provisionalFingerprintHoldTimer
|
||||
interval: 50
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
const elapsed = Date.now() - root.provisionalFingerprintHoldStartedAt;
|
||||
root.provisionalFingerprintHoldProgress = Math.min(1,
|
||||
elapsed / Math.max(1, root.provisionalFingerprintHoldMs));
|
||||
if (root.provisionalFingerprintHoldProgress >= 1)
|
||||
root.confirmProvisionalFingerprintHold();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: provisionalFingerprintConfirmTimer
|
||||
interval: 3500
|
||||
onTriggered: root.provisionalFingerprintConfirmed = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: provisionalFingerprintPulseTimer
|
||||
interval: 3500
|
||||
onTriggered: root.provisionalFingerprintPulseSeen = false
|
||||
FingerprintPreview.reset("lock");
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
|
|
|||
193
surfaces/quickshell/modules/ii/polkit/PolkitContent.qml
Normal file
193
surfaces/quickshell/modules/ii/polkit/PolkitContent.qml
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Phone Polkit content with a first-class FPC1020 temporary factor.
|
||||
//
|
||||
// The PAM module owns acceptance. This surface only recognizes its prompt,
|
||||
// waits for a pulse that came from blueline-fingerprintd (not generic wake
|
||||
// input), and submits the blank PAM response after the visible confirmation
|
||||
// interval. The normal PIN/password conversation stays intact as fallback.
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
|
||||
readonly property bool fpcPrompt:
|
||||
FingerprintPreview.polkitEnabled
|
||||
&& PolkitService.cleanPrompt === "Touch and hold the fingerprint reader"
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
PolkitService.cancel();
|
||||
}
|
||||
|
||||
function submitPassword() {
|
||||
PolkitService.submit(inputField.text);
|
||||
}
|
||||
|
||||
function usePinInstead() {
|
||||
// An empty response tells pam_souveraine_fpc to decline. PAM then
|
||||
// reaches the ordinary system-auth conversation, where this same
|
||||
// window shows the normal password field.
|
||||
PolkitService.submit("");
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService
|
||||
function onInteractionAvailableChanged() {
|
||||
if (!PolkitService.interactionAvailable || root.fpcPrompt)
|
||||
return;
|
||||
inputField.text = "";
|
||||
inputField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: FingerprintPreview
|
||||
function onPulseObserved() {
|
||||
if (PolkitService.active && PolkitService.interactionAvailable && root.fpcPrompt)
|
||||
FingerprintPreview.beginHold("polkit");
|
||||
}
|
||||
function onHoldConfirmed(purpose) {
|
||||
if (purpose === "polkit" && root.fpcPrompt && PolkitService.interactionAvailable)
|
||||
PolkitService.submit("");
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colScrim
|
||||
opacity: 0
|
||||
Component.onCompleted: opacity = 1
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialog {
|
||||
anchors.centerIn: parent
|
||||
backgroundWidth: 450
|
||||
show: false
|
||||
Component.onCompleted: show = true
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
iconSize: 26
|
||||
text: root.fpcPrompt ? "fingerprint" : "security"
|
||||
color: Appearance.colors.colSecondary
|
||||
}
|
||||
|
||||
WindowDialogTitle {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Translation.tr("Authentication")
|
||||
}
|
||||
|
||||
WindowDialogParagraph {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: PolkitService.cleanMessage
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
visible: root.fpcPrompt
|
||||
implicitHeight: visible ? 104 : 0
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: FingerprintPreview.confirmedPurpose === "polkit"
|
||||
? Appearance.colors.colPrimary : "#2a000000"
|
||||
border.width: FingerprintPreview.pulseSeen ? 2 : 1
|
||||
border.color: FingerprintPreview.pulseSeen
|
||||
? Appearance.colors.colPrimary : "#66ffffff"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * (FingerprintPreview.activePurpose === "polkit"
|
||||
? FingerprintPreview.holdProgress : 0)
|
||||
height: 3
|
||||
radius: 2
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "fingerprint"
|
||||
iconSize: 32
|
||||
color: FingerprintPreview.confirmedPurpose === "polkit"
|
||||
? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer1
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: FingerprintPreview.activePurpose === "polkit"
|
||||
? Translation.tr("Reader contact received — confirming")
|
||||
: Translation.tr("Touch and hold the fingerprint reader")
|
||||
color: FingerprintPreview.confirmedPurpose === "polkit"
|
||||
? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: FingerprintPreview.activePurpose === "polkit"
|
||||
text: Translation.tr("%1 seconds").arg(Math.round(FingerprintPreview.holdMs / 1000))
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTextField {
|
||||
id: inputField
|
||||
Layout.fillWidth: true
|
||||
visible: !root.fpcPrompt
|
||||
focus: visible
|
||||
enabled: PolkitService.interactionAvailable
|
||||
placeholderText: PolkitService.cleanPrompt
|
||||
echoMode: root.usePasswordChars ? TextInput.Password : TextInput.Normal
|
||||
onAccepted: root.submitPassword()
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
PolkitService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogButtonRow {
|
||||
Layout.bottomMargin: 10
|
||||
Item { Layout.fillWidth: true }
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: PolkitService.cancel()
|
||||
}
|
||||
DialogButton {
|
||||
visible: root.fpcPrompt
|
||||
enabled: PolkitService.interactionAvailable
|
||||
buttonText: Translation.tr("Use PIN instead")
|
||||
onClicked: root.usePinInstead()
|
||||
}
|
||||
DialogButton {
|
||||
visible: !root.fpcPrompt
|
||||
enabled: PolkitService.interactionAvailable
|
||||
buttonText: Translation.tr("OK")
|
||||
onClicked: root.submitPassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFpcPromptChanged: {
|
||||
if (!fpcPrompt)
|
||||
FingerprintPreview.reset("polkit");
|
||||
}
|
||||
}
|
||||
|
|
@ -289,7 +289,7 @@ ContentPage {
|
|||
checked: Config.options.lock.fingerprintPreview.enabled
|
||||
onCheckedChanged: Config.options.lock.fingerprintPreview.enabled = checked
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Shows a three-second hold test on the lock screen. It never unlocks the device or grants step-up; it is only the UI and sensor-routing preview.")
|
||||
text: Translation.tr("Shows a three-second hold test on the lock screen. It never unlocks the device; post-login Polkit is configured separately below.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +303,17 @@ ContentPage {
|
|||
enabled: Config.options.lock.fingerprintPreview.enabled
|
||||
onValueChanged: Config.options.lock.fingerprintPreview.holdMs = value * 1000
|
||||
StyledToolTip {
|
||||
text: Translation.tr("This controls only the visible hold exercise. A real fingerprint match must arrive through the attested FPC daemon before it can authenticate anything.")
|
||||
text: Translation.tr("This controls the visible lock-screen exercise only. It does not change the temporary post-login Polkit factor.")
|
||||
}
|
||||
}
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "admin_panel_settings"
|
||||
text: Translation.tr("Allow temporary FPC confirmation for Polkit")
|
||||
checked: Config.options.lock.fingerprintPolkit.enabled
|
||||
onCheckedChanged: Config.options.lock.fingerprintPolkit.enabled = checked
|
||||
StyledToolTip {
|
||||
text: Translation.tr("After PIN login, user-facing Polkit prompts may accept a fresh reader assertion after the visible confirmation interval. It never unlocks the session or replaces first-login PIN.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
136
surfaces/quickshell/services/FingerprintPreview.qml
Normal file
136
surfaces/quickshell/services/FingerprintPreview.qml
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// One owner for the temporary FPC1020 interaction path.
|
||||
//
|
||||
// The privileged reader daemon publishes only an IRQ pulse. Until the
|
||||
// match-on-chip daemon exists, a user-visible hold is the configured temporary
|
||||
// confirmation. The Polkit surface may submit the configured temporary PAM
|
||||
// factor after a completed hold; it is never a lock-screen unlock path.
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property bool previewEnabled:
|
||||
Config.options?.lock?.fingerprintPreview?.enabled ?? false
|
||||
readonly property bool polkitEnabled:
|
||||
Config.options?.lock?.fingerprintPolkit?.enabled ?? true
|
||||
readonly property bool enabled: previewEnabled || polkitEnabled
|
||||
readonly property int holdMs:
|
||||
Config.options?.lock?.fingerprintPreview?.holdMs ?? 3000
|
||||
property bool holding: false
|
||||
property bool confirmed: false
|
||||
property bool pulseSeen: false
|
||||
property real holdProgress: 0
|
||||
property double holdStartedAt: 0
|
||||
property string activePurpose: ""
|
||||
property string confirmedPurpose: ""
|
||||
property string pulseToken: ""
|
||||
|
||||
signal pulseObserved()
|
||||
signal holdConfirmed(string purpose)
|
||||
|
||||
function beginHold(purpose) {
|
||||
if (!root.enabled || !purpose) return false;
|
||||
root.confirmed = false;
|
||||
root.confirmedPurpose = "";
|
||||
root.activePurpose = purpose;
|
||||
root.holding = true;
|
||||
root.holdStartedAt = Date.now();
|
||||
root.holdProgress = 0;
|
||||
holdTimer.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
function cancelHold(purpose = "") {
|
||||
if (purpose && root.activePurpose !== purpose) return;
|
||||
root.holding = false;
|
||||
root.holdProgress = 0;
|
||||
root.activePurpose = "";
|
||||
holdTimer.stop();
|
||||
}
|
||||
|
||||
function confirmHold(purpose = "") {
|
||||
if (!root.holding || (purpose && root.activePurpose !== purpose)) return;
|
||||
const confirmedPurpose = root.activePurpose;
|
||||
root.holding = false;
|
||||
root.holdProgress = 1;
|
||||
root.activePurpose = "";
|
||||
holdTimer.stop();
|
||||
root.confirmed = true;
|
||||
root.confirmedPurpose = confirmedPurpose;
|
||||
root.holdConfirmed(confirmedPurpose);
|
||||
confirmTimer.restart();
|
||||
}
|
||||
|
||||
// The diagnostic IPC and the reader file meet here. Neither can unlock a
|
||||
// session; Polkit alone may use the explicit temporary confirmation path.
|
||||
function notePulse() {
|
||||
if (!root.enabled)
|
||||
return { ok: false, code: "not_enabled", reason: "fingerprint preview is disabled" };
|
||||
root.pulseSeen = true;
|
||||
root.pulseObserved();
|
||||
pulseTimer.restart();
|
||||
return { ok: true, status: "observed" };
|
||||
}
|
||||
|
||||
function reset(purpose = "") {
|
||||
if (purpose && root.activePurpose !== purpose && root.confirmedPurpose !== purpose)
|
||||
return;
|
||||
root.cancelHold();
|
||||
root.confirmed = false;
|
||||
root.confirmedPurpose = "";
|
||||
root.pulseSeen = false;
|
||||
confirmTimer.stop();
|
||||
pulseTimer.stop();
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: pulseFile
|
||||
path: "/run/blueline-fingerprintd/preview-pulse"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
const record = JSON.parse(pulseFile.text());
|
||||
const token = `${record.sequence}:${record.at_ms}`;
|
||||
if (token === root.pulseToken) return;
|
||||
root.pulseToken = token;
|
||||
const ageMs = Date.now() - Number(record.at_ms);
|
||||
if (Number(record.sequence) > 0 && ageMs >= 0 && ageMs < 5000)
|
||||
root.notePulse();
|
||||
} catch (error) {
|
||||
console.warn("[fingerprint-preview] invalid FPC pulse record:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: holdTimer
|
||||
interval: 50
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
const elapsed = Date.now() - root.holdStartedAt;
|
||||
root.holdProgress = Math.min(1, elapsed / Math.max(1, root.holdMs));
|
||||
if (root.holdProgress >= 1) root.confirmHold();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: confirmTimer
|
||||
interval: 3500
|
||||
onTriggered: {
|
||||
root.confirmed = false;
|
||||
root.confirmedPurpose = "";
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: pulseTimer
|
||||
interval: 3500
|
||||
onTriggered: root.pulseSeen = false
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ singleton SessiondBridge 1.0 SessiondBridge.qml
|
|||
singleton SessiondPolicy 1.0 SessiondPolicy.qml
|
||||
singleton SongRec 1.0 SongRec.qml
|
||||
singleton Face 1.0 Face.qml
|
||||
singleton FingerprintPreview 1.0 FingerprintPreview.qml
|
||||
singleton Souveraine 1.0 Souveraine.qml
|
||||
singleton Speech 1.0 Speech.qml
|
||||
singleton StepUpAuth 1.0 StepUpAuth.qml
|
||||
|
|
|
|||
Loading…
Reference in a new issue