Enhance SandwichReminder-AutoOrder feature with direct checkout navigation and improved logic
- Added new configuration options for direct checkout navigation, including `useDirectCheckoutNavigation`, `checkoutPath`, and `checkoutOpenDelayMs`. - Updated the auto-order flow to navigate directly to the checkout page, skipping the mini-cart and date/time steps. - Improved keyboard automation logic for item remark and order confirmation processes. - Removed the old SandwichAutoOrder.ps1 file as its functionality has been integrated into SandwichReminder-AutoOrder.ps1. - Introduced a new runner-launcher.vbs to start the runner.ps1 without a visible console window, ensuring WinForms dialogs can appear. - Implemented a mutex mechanism in runner.ps1 to prevent concurrent execution of the same feature.
This commit is contained in:
@@ -38,12 +38,11 @@ ArnePowershellAutomation/
|
|||||||
│ ├── NetworkUtils.ps1 # DNS suffix detection
|
│ ├── NetworkUtils.ps1 # DNS suffix detection
|
||||||
│ ├── ToastHelper.ps1 # Windows 11 toast notifications
|
│ ├── ToastHelper.ps1 # Windows 11 toast notifications
|
||||||
│ ├── PromptHelper.ps1 # Multi-choice dialog boxes (up to 4 buttons)
|
│ ├── PromptHelper.ps1 # Multi-choice dialog boxes (up to 4 buttons)
|
||||||
│ └── SandwichAutoOrder.ps1 # Browser keyboard automation for sandwich auto-order
|
|
||||||
├── features/ # Automation feature modules
|
├── features/ # Automation feature modules
|
||||||
│ ├── DynamicLock.ps1 # Toggle Dynamic Lock on network presence
|
│ ├── DynamicLock.ps1 # Toggle Dynamic Lock on network presence
|
||||||
│ ├── DefaultBrowser.ps1 # Monitor default browser setting
|
│ ├── DefaultBrowser.ps1 # Monitor default browser setting
|
||||||
│ ├── SandwichReminder.ps1 # Time-based reminder with snooze options
|
│ ├── SandwichReminder.ps1 # Time-based reminder with snooze options
|
||||||
│ └── SandwichReminder-AutoOrder.ps1 # Personal browser automation for sandwich ordering
|
│ └── SandwichReminder-AutoOrder.ps1 # Personal browser automation for sandwich ordering (self-contained flow)
|
||||||
└── data/
|
└── data/
|
||||||
├── config.json # Feature toggles & settings (auto-seeded)
|
├── config.json # Feature toggles & settings (auto-seeded)
|
||||||
├── state/
|
├── state/
|
||||||
@@ -140,11 +139,11 @@ Shows a reminder dialog at a specific time on a specific network with snooze opt
|
|||||||
### SandwichReminder-AutoOrder
|
### SandwichReminder-AutoOrder
|
||||||
Personal browser automation feature that drives the sandwich ordering website via keyboard navigation.
|
Personal browser automation feature that drives the sandwich ordering website via keyboard navigation.
|
||||||
- **Settings**: Base URL, item ID, browser window title hint, tab counts for each navigation step, delays, calibration mode
|
- **Settings**: Base URL, item ID, browser window title hint, tab counts for each navigation step, delays, calibration mode
|
||||||
- **Logic**: Opens the item modal URL, brings the browser window to the foreground (with retry), then sends keyboard inputs to: toggle option checkboxes → click add-to-cart → refresh page → tab to mini-cart → open cart → tab to confirm order button → open delivery popup
|
- **Logic**: Opens the item modal URL, brings the browser window to the foreground (with retry), then sends keyboard inputs to: toggle option checkboxes → click add-to-cart → navigate directly to `/checkout` → fill remarks → confirm order → confirm default payment option
|
||||||
- **Key settings**:
|
- **Key settings**:
|
||||||
- `tabsToOption1` / `tabsBetweenOptions` / `tabsToAddButton`: navigation within item modal
|
- `tabsToOption1` / `tabsBetweenOptions` / `tabsToAddButton`: navigation within item modal
|
||||||
- `refreshBeforeCart`: send F5 after add-to-cart to reset focus to a known page position
|
- `useDirectCheckoutNavigation` + `checkoutPath`: skip mini-cart/date-time by opening checkout directly
|
||||||
- `tabsToCartButton` / `tabsToConfirmButton`: navigation after refresh
|
- `tabsToItemRemarkButton` / `tabsToOrderRemarkInput` / `tabsToFinalConfirmButton` / `tabsToPaymentOption`: navigation on checkout and payment screens
|
||||||
- `calibrationOnly` + `calibrationTabs`: safe mode to manually count tab stops without clicking
|
- `calibrationOnly` + `calibrationTabs`: safe mode to manually count tab stops without clicking
|
||||||
- **Note**: Uses WScript.Shell SendKeys (best-effort); relies on stable tab order in the target website
|
- **Note**: Uses WScript.Shell SendKeys (best-effort); relies on stable tab order in the target website
|
||||||
|
|
||||||
@@ -190,7 +189,7 @@ When you select a feature for configuration, you'll be prompted for each setting
|
|||||||
- **Windows 11 Only for Toasts**: Toast notifications require WinRT (Windows 10+), but full Windows 11 support is assumed. Win10 may work but is untested.
|
- **Windows 11 Only for Toasts**: Toast notifications require WinRT (Windows 10+), but full Windows 11 support is assumed. Win10 may work but is untested.
|
||||||
- **Scheduled Task Security**: The runner task executes as the current user with Limited RunLevel (no elevation). Features cannot perform system-wide administrative tasks; they're limited to user-level operations.
|
- **Scheduled Task Security**: The runner task executes as the current user with Limited RunLevel (no elevation). Features cannot perform system-wide administrative tasks; they're limited to user-level operations.
|
||||||
- **DNS Suffix Matching**: Network detection relies on `Get-DnsClient` adapter DNS suffix. Make sure your network adapter is configured with the correct suffix for accurate detection.
|
- **DNS Suffix Matching**: Network detection relies on `Get-DnsClient` adapter DNS suffix. Make sure your network adapter is configured with the correct suffix for accurate detection.
|
||||||
- **Manual Execution & Repetition Timer**: Executing the runner manually via menu option 5 ("Execute runner now") runs the task immediately but does **not** start the 2-minute repetition timer. The repetition timer only activates on logon/restart. For continuous testing, you may need to restart or wait for next logon.
|
- **Manual Execution & Repetition Timer**: Executing the runner manually via menu option 5 ("Execute runner now") launches the runner directly via a hidden launcher and does **not** start the 2-minute repetition timer. The repetition timer only activates on logon/restart.
|
||||||
|
|
||||||
## Extending with New Features
|
## Extending with New Features
|
||||||
|
|
||||||
|
|||||||
+39
-9
@@ -11,7 +11,7 @@ $ErrorActionPreference = 'Continue'
|
|||||||
|
|
||||||
$script:InternalRoot = Join-Path $PSScriptRoot 'internal'
|
$script:InternalRoot = Join-Path $PSScriptRoot 'internal'
|
||||||
$script:TaskName = 'PSAutomation-Runner'
|
$script:TaskName = 'PSAutomation-Runner'
|
||||||
$script:TaskPath = '\'
|
$script:TaskPath = '\PowerShell Automation\'
|
||||||
$script:RunnerIntervalMinutes = 2
|
$script:RunnerIntervalMinutes = 2
|
||||||
$script:ConfigureScriptPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.MyCommand.Path }
|
$script:ConfigureScriptPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.MyCommand.Path }
|
||||||
$script:RegisterResultPath = Join-Path $script:InternalRoot 'data\logs\last-register-result.json'
|
$script:RegisterResultPath = Join-Path $script:InternalRoot 'data\logs\last-register-result.json'
|
||||||
@@ -394,6 +394,7 @@ function Show-RegistrationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$runnerPath = Join-Path $script:InternalRoot 'runner.ps1'
|
$runnerPath = Join-Path $script:InternalRoot 'runner.ps1'
|
||||||
|
$runnerLauncherPath = Join-Path $script:InternalRoot 'runner-launcher.vbs'
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host " Runner: $runnerPath" -ForegroundColor DarkGray
|
Write-Host " Runner: $runnerPath" -ForegroundColor DarkGray
|
||||||
Write-Log -Level Info -Message ("Task found at path '{0}' with state '{1}'." -f $task.TaskPath, $task.State) -Feature 'Configure'
|
Write-Log -Level Info -Message ("Task found at path '{0}' with state '{1}'." -f $task.TaskPath, $task.State) -Feature 'Configure'
|
||||||
@@ -415,6 +416,7 @@ function Register-Runner {
|
|||||||
Write-Log -Level Info -Message 'Register-Runner invoked.' -Feature 'Configure'
|
Write-Log -Level Info -Message 'Register-Runner invoked.' -Feature 'Configure'
|
||||||
|
|
||||||
$runnerPath = Join-Path $script:InternalRoot 'runner.ps1'
|
$runnerPath = Join-Path $script:InternalRoot 'runner.ps1'
|
||||||
|
$runnerLauncherPath = Join-Path $script:InternalRoot 'runner-launcher.vbs'
|
||||||
|
|
||||||
# Request elevation only for this operation, when needed.
|
# Request elevation only for this operation, when needed.
|
||||||
if (-not (Test-Administrator)) {
|
if (-not (Test-Administrator)) {
|
||||||
@@ -474,6 +476,14 @@ function Register-Runner {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $runnerLauncherPath)) {
|
||||||
|
Write-Host " ERROR: runner-launcher.vbs not found at: $runnerLauncherPath" -ForegroundColor Red
|
||||||
|
Write-Log -Level Error -Message "runner-launcher.vbs missing at '$runnerLauncherPath'." -Feature 'Configure'
|
||||||
|
Write-OperationResult -Path $script:RegisterResultPath -Success $false -Message "runner-launcher.vbs missing at '$runnerLauncherPath'."
|
||||||
|
if (-not $NoPause) { Pause-ForKey }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
$existing = Get-RunnerTask
|
$existing = Get-RunnerTask
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
Write-Host " Task '$($script:TaskName)' already exists (state: $($existing.State))." -ForegroundColor Yellow
|
Write-Host " Task '$($script:TaskName)' already exists (state: $($existing.State))." -ForegroundColor Yellow
|
||||||
@@ -504,8 +514,8 @@ function Register-Runner {
|
|||||||
try {
|
try {
|
||||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
$action = New-ScheduledTaskAction `
|
$action = New-ScheduledTaskAction `
|
||||||
-Execute 'powershell.exe' `
|
-Execute 'wscript.exe' `
|
||||||
-Argument "-WindowStyle Hidden -NonInteractive -ExecutionPolicy Bypass -File `"$runnerPath`""
|
-Argument "//B //Nologo `"$runnerLauncherPath`" `"$runnerPath`""
|
||||||
|
|
||||||
# Logon trigger — copy Repetition from a -Once trigger (reliable workaround)
|
# Logon trigger — copy Repetition from a -Once trigger (reliable workaround)
|
||||||
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn
|
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn
|
||||||
@@ -766,9 +776,21 @@ function Initialize-FeatureCache {
|
|||||||
# ═════════════════════════════════════════════════════════════════════════════
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
function Invoke-RunnerNow {
|
function Invoke-RunnerNow {
|
||||||
|
$runnerPath = Join-Path $script:InternalRoot 'runner.ps1'
|
||||||
|
$runnerLauncherPath = Join-Path $script:InternalRoot 'runner-launcher.vbs'
|
||||||
$task = Get-RunnerTask
|
$task = Get-RunnerTask
|
||||||
if (-not $task) {
|
|
||||||
Write-Host " ERROR: Task is not registered. Use option 2 to register first." -ForegroundColor Red
|
if (-not (Test-Path $runnerPath)) {
|
||||||
|
Write-Host " ERROR: runner.ps1 not found at: $runnerPath" -ForegroundColor Red
|
||||||
|
Write-Log -Level Error -Message "runner.ps1 missing at '$runnerPath'." -Feature 'Configure'
|
||||||
|
Write-Host ''
|
||||||
|
Pause-ForKey
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $runnerLauncherPath)) {
|
||||||
|
Write-Host " ERROR: runner-launcher.vbs not found at: $runnerLauncherPath" -ForegroundColor Red
|
||||||
|
Write-Log -Level Error -Message "runner-launcher.vbs missing at '$runnerLauncherPath'." -Feature 'Configure'
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Pause-ForKey
|
Pause-ForKey
|
||||||
return
|
return
|
||||||
@@ -776,15 +798,23 @@ function Invoke-RunnerNow {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host " Executing runner task now..." -ForegroundColor Cyan
|
if ($task) {
|
||||||
Start-ScheduledTask -TaskName $script:TaskName -TaskPath $script:TaskPath -ErrorAction Stop
|
Write-Host " Executing registered runner task now..." -ForegroundColor Cyan
|
||||||
|
Start-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -ErrorAction Stop
|
||||||
Write-Host " Task execution started (runs async). Check logs in a moment." -ForegroundColor Green
|
Write-Host " Task execution started (runs async). Check logs in a moment." -ForegroundColor Green
|
||||||
Write-Hint " Note: The 2-minute repetition timer only starts on next logon/restart."
|
Write-Hint " Note: The 2-minute repetition timer only starts on next logon/restart."
|
||||||
Write-Log -Level Info -Message "Runner task invoked manually from configure menu." -Feature 'Configure'
|
Write-Log -Level Info -Message "Runner task invoked manually from configure menu." -Feature 'Configure'
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Write-Host " Task not registered; executing runner directly (hidden)..." -ForegroundColor Yellow
|
||||||
|
Start-Process -FilePath 'wscript.exe' -ArgumentList "//B //Nologo `"$runnerLauncherPath`" `"$runnerPath`"" -WindowStyle Hidden
|
||||||
|
Write-Host " Runner launch started (runs async). Check logs in a moment." -ForegroundColor Green
|
||||||
|
Write-Log -Level Info -Message "Runner invoked manually from configure menu via hidden launcher (task absent)." -Feature 'Configure'
|
||||||
|
}
|
||||||
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host " ERROR: Failed to start task: $_" -ForegroundColor Red
|
Write-Host " ERROR: Failed to start runner: $_" -ForegroundColor Red
|
||||||
Write-Log -Level Error -Message "Failed to start runner task manually: $_" -Feature 'Configure'
|
Write-Log -Level Error -Message "Failed to start runner manually: $_" -Feature 'Configure'
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
|
|||||||
@@ -100,6 +100,27 @@ $FeatureMeta = @{
|
|||||||
Default = 6
|
Default = 6
|
||||||
Description = 'Tab count from second checkbox to add-to-cart button'
|
Description = 'Tab count from second checkbox to add-to-cart button'
|
||||||
},
|
},
|
||||||
|
@{
|
||||||
|
Key = 'useDirectCheckoutNavigation'
|
||||||
|
Label = 'Use Direct Checkout Navigation'
|
||||||
|
Type = 'bool'
|
||||||
|
Default = $true
|
||||||
|
Description = 'After add-to-cart, navigate directly to /checkout to skip mini-cart and date/time steps'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'checkoutPath'
|
||||||
|
Label = 'Checkout Path'
|
||||||
|
Type = 'string'
|
||||||
|
Default = '/checkout'
|
||||||
|
Description = 'Relative path used for direct checkout navigation'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'checkoutOpenDelayMs'
|
||||||
|
Label = 'Checkout Open Delay (ms)'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 1800
|
||||||
|
Description = 'Wait time after opening checkout page before continuing keyboard flow'
|
||||||
|
},
|
||||||
@{
|
@{
|
||||||
Key = 'openCartPopupAfterAdd'
|
Key = 'openCartPopupAfterAdd'
|
||||||
Label = 'Open Cart Popup After Add'
|
Label = 'Open Cart Popup After Add'
|
||||||
@@ -134,10 +155,407 @@ $FeatureMeta = @{
|
|||||||
Type = 'int'
|
Type = 'int'
|
||||||
Default = 3
|
Default = 3
|
||||||
Description = 'Tab count from mini-cart button to confirm order button (opens delivery popup)'
|
Description = 'Tab count from mini-cart button to confirm order button (opens delivery popup)'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsToDateTimeConfirm'
|
||||||
|
Label = 'Tabs To Date Time Confirm'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 4
|
||||||
|
Description = 'Tab count on delivery popup to confirm prefilled day and time'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsToItemRemarkButton'
|
||||||
|
Label = 'Tabs To Item Remark Button'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 8
|
||||||
|
Description = 'Tab count on order overview to open item remark popup'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsToCloseItemRemarkPopup'
|
||||||
|
Label = 'Tabs To Close Item Remark Popup'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 1
|
||||||
|
Description = 'Tab count after filling item remark to reach confirm button in popup'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsBeforeItemRemarkInput'
|
||||||
|
Label = 'Tabs Before Item Remark Input'
|
||||||
|
Type = 'int'
|
||||||
|
Default = -5
|
||||||
|
Description = 'Tab count inside item remark popup before typing (negative means SHIFT+TAB)'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsToOrderRemarkInput'
|
||||||
|
Label = 'Tabs To Order Remark Input'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 6
|
||||||
|
Description = 'Tab count from item remark popup close to order remark input'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsToFinalConfirmButton'
|
||||||
|
Label = 'Tabs To Final Confirm Button'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 3
|
||||||
|
Description = 'Tab count from order remark input to final confirm button'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'tabsToPaymentOption'
|
||||||
|
Label = 'Tabs To Payment Option'
|
||||||
|
Type = 'int'
|
||||||
|
Default = -9
|
||||||
|
Description = 'Tab count on payment option screen to default option (negative means SHIFT+TAB)'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'transitionDelayMs'
|
||||||
|
Label = 'Transition Delay (ms)'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 400
|
||||||
|
Description = 'Wait time after major Enter actions while screens or popups render'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'deliveryPopupRenderDelayMs'
|
||||||
|
Label = 'Delivery Popup Render Delay (ms)'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 1200
|
||||||
|
Description = 'Extra wait right before the 4-tab date/time confirm step'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'checkoutOverviewRenderDelayMs'
|
||||||
|
Label = 'Checkout Overview Render Delay (ms)'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 1200
|
||||||
|
Description = 'Extra wait after confirming date/time before running 3-tab item remark navigation'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = 'itemRemarkPopupRenderDelayMs'
|
||||||
|
Label = 'Item Remark Popup Render Delay (ms)'
|
||||||
|
Type = 'int'
|
||||||
|
Default = 1200
|
||||||
|
Description = 'Extra wait after opening item remark popup before tabbing and typing'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Invoke-AppActivateBestEffort {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]$Shell,
|
||||||
|
[Parameter(Mandatory)][string[]]$TitleCandidates,
|
||||||
|
[int]$TimeoutMs = 10000,
|
||||||
|
[int]$RetryDelayMs = 300
|
||||||
|
)
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddMilliseconds($TimeoutMs)
|
||||||
|
$candidates = @($TitleCandidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
|
|
||||||
|
do {
|
||||||
|
foreach ($candidate in $candidates) {
|
||||||
|
if ($Shell.AppActivate($candidate)) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds $RetryDelayMs
|
||||||
|
} while ((Get-Date) -lt $deadline)
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-SendKeysSafeText {
|
||||||
|
param(
|
||||||
|
[string]$Text
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Text) { return '' }
|
||||||
|
|
||||||
|
# Escape SendKeys reserved characters so user remarks are typed literally.
|
||||||
|
$escaped = $Text
|
||||||
|
$escaped = $escaped -replace '(\+|\^|%|~|\(|\)|\[|\]|\{|\})', '{$1}'
|
||||||
|
return $escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-TabSteps {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]$Shell,
|
||||||
|
[int]$Count,
|
||||||
|
[int]$StepDelayMs
|
||||||
|
)
|
||||||
|
|
||||||
|
$key = if ($Count -lt 0) { '+{TAB}' } else { '{TAB}' }
|
||||||
|
$iterations = [Math]::Abs($Count)
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $iterations; $i++) {
|
||||||
|
$Shell.SendKeys($key)
|
||||||
|
Start-Sleep -Milliseconds $StepDelayMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SandwichAutoOrderFlow {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][hashtable]$AutoOrderConfig,
|
||||||
|
[Parameter(Mandatory)][string]$FallbackUrl,
|
||||||
|
[string]$FeatureName = 'SandwichReminder-AutoOrder'
|
||||||
|
)
|
||||||
|
|
||||||
|
$baseUrl = [string]$AutoOrderConfig['baseUrl']
|
||||||
|
if ([string]::IsNullOrWhiteSpace($baseUrl)) { $baseUrl = 'https://ylos-kitchen.unipage.eu' }
|
||||||
|
|
||||||
|
$itemId = [string]$AutoOrderConfig['itemId']
|
||||||
|
if ([string]::IsNullOrWhiteSpace($itemId)) { $itemId = 'dmwmo3' }
|
||||||
|
|
||||||
|
$windowTitleHint = [string]$AutoOrderConfig['windowTitleHint']
|
||||||
|
if ([string]::IsNullOrWhiteSpace($windowTitleHint)) { $windowTitleHint = "YLO'S Kitchen" }
|
||||||
|
|
||||||
|
$initialDelayMs = [int]$AutoOrderConfig['initialDelayMs']
|
||||||
|
if ($initialDelayMs -le 0) { $initialDelayMs = 2500 }
|
||||||
|
|
||||||
|
$stepDelayMs = [int]$AutoOrderConfig['stepDelayMs']
|
||||||
|
if ($stepDelayMs -le 0) { $stepDelayMs = 120 }
|
||||||
|
|
||||||
|
$toggleKeySetting = [string]$AutoOrderConfig['optionToggleKey']
|
||||||
|
if ([string]::IsNullOrWhiteSpace($toggleKeySetting)) { $toggleKeySetting = 'SPACE' }
|
||||||
|
$toggleKey = if ($toggleKeySetting.ToUpperInvariant() -eq 'ENTER') { '{ENTER}' } else { ' ' }
|
||||||
|
|
||||||
|
$calibrationOnly = [bool]$AutoOrderConfig['calibrationOnly']
|
||||||
|
$calibrationTabs = [int]$AutoOrderConfig['calibrationTabs']
|
||||||
|
if ($calibrationTabs -lt 0) { $calibrationTabs = 0 }
|
||||||
|
|
||||||
|
$tabsToOption1 = [int]$AutoOrderConfig['tabsToOption1']
|
||||||
|
if ($tabsToOption1 -lt 0) { $tabsToOption1 = 3 }
|
||||||
|
|
||||||
|
$tabsBetweenOptions = [int]$AutoOrderConfig['tabsBetweenOptions']
|
||||||
|
if ($tabsBetweenOptions -lt 0) { $tabsBetweenOptions = 2 }
|
||||||
|
|
||||||
|
$tabsToAddButton = [int]$AutoOrderConfig['tabsToAddButton']
|
||||||
|
if ($tabsToAddButton -lt 0) { $tabsToAddButton = 4 }
|
||||||
|
|
||||||
|
$useDirectCheckoutNavigation = [bool]$AutoOrderConfig['useDirectCheckoutNavigation']
|
||||||
|
$checkoutPath = [string]$AutoOrderConfig['checkoutPath']
|
||||||
|
if ([string]::IsNullOrWhiteSpace($checkoutPath)) { $checkoutPath = '/checkout' }
|
||||||
|
if (-not $checkoutPath.StartsWith('/')) { $checkoutPath = '/' + $checkoutPath }
|
||||||
|
$checkoutOpenDelayMs = [int]$AutoOrderConfig['checkoutOpenDelayMs']
|
||||||
|
if ($checkoutOpenDelayMs -le 0) { $checkoutOpenDelayMs = 1800 }
|
||||||
|
|
||||||
|
$openCartPopup = [bool]$AutoOrderConfig['openCartPopupAfterAdd']
|
||||||
|
$refreshBeforeCart = [bool]$AutoOrderConfig['refreshBeforeCart']
|
||||||
|
$postRefreshDelayMs = [int]$AutoOrderConfig['postRefreshDelayMs']
|
||||||
|
if ($postRefreshDelayMs -le 0) { $postRefreshDelayMs = 2500 }
|
||||||
|
$tabsToCartButton = [int]$AutoOrderConfig['tabsToCartButton']
|
||||||
|
if ($tabsToCartButton -le 0) { $tabsToCartButton = 5 }
|
||||||
|
$tabsToConfirmButton = [int]$AutoOrderConfig['tabsToConfirmButton']
|
||||||
|
if ($tabsToConfirmButton -le 0) { $tabsToConfirmButton = 3 }
|
||||||
|
$tabsToDateTimeConfirm = [int]$AutoOrderConfig['tabsToDateTimeConfirm']
|
||||||
|
if ($tabsToDateTimeConfirm -le 0) { $tabsToDateTimeConfirm = 4 }
|
||||||
|
$tabsToItemRemarkButton = [int]$AutoOrderConfig['tabsToItemRemarkButton']
|
||||||
|
if ($tabsToItemRemarkButton -le 0) { $tabsToItemRemarkButton = 8 }
|
||||||
|
$tabsToCloseItemRemarkPopup = [int]$AutoOrderConfig['tabsToCloseItemRemarkPopup']
|
||||||
|
if ($tabsToCloseItemRemarkPopup -le 0) { $tabsToCloseItemRemarkPopup = 1 }
|
||||||
|
$tabsBeforeItemRemarkInput = [int]$AutoOrderConfig['tabsBeforeItemRemarkInput']
|
||||||
|
if ($tabsBeforeItemRemarkInput -eq 0) { $tabsBeforeItemRemarkInput = -5 }
|
||||||
|
$tabsToOrderRemarkInput = [int]$AutoOrderConfig['tabsToOrderRemarkInput']
|
||||||
|
if ($tabsToOrderRemarkInput -le 0) { $tabsToOrderRemarkInput = 6 }
|
||||||
|
$tabsToFinalConfirmButton = [int]$AutoOrderConfig['tabsToFinalConfirmButton']
|
||||||
|
if ($tabsToFinalConfirmButton -le 0) { $tabsToFinalConfirmButton = 3 }
|
||||||
|
$tabsToPaymentOption = [int]$AutoOrderConfig['tabsToPaymentOption']
|
||||||
|
if ($tabsToPaymentOption -eq 0) { $tabsToPaymentOption = -9 }
|
||||||
|
$transitionDelayMs = [int]$AutoOrderConfig['transitionDelayMs']
|
||||||
|
if ($transitionDelayMs -le 0) { $transitionDelayMs = 400 }
|
||||||
|
$deliveryPopupRenderDelayMs = [int]$AutoOrderConfig['deliveryPopupRenderDelayMs']
|
||||||
|
if ($deliveryPopupRenderDelayMs -le 0) { $deliveryPopupRenderDelayMs = 1200 }
|
||||||
|
$checkoutOverviewRenderDelayMs = [int]$AutoOrderConfig['checkoutOverviewRenderDelayMs']
|
||||||
|
if ($checkoutOverviewRenderDelayMs -le 0) { $checkoutOverviewRenderDelayMs = 1200 }
|
||||||
|
$itemRemarkPopupRenderDelayMs = [int]$AutoOrderConfig['itemRemarkPopupRenderDelayMs']
|
||||||
|
if ($itemRemarkPopupRenderDelayMs -le 0) { $itemRemarkPopupRenderDelayMs = 1200 }
|
||||||
|
|
||||||
|
$activationTimeoutMs = [int]$AutoOrderConfig['activationTimeoutMs']
|
||||||
|
if ($activationTimeoutMs -le 0) { $activationTimeoutMs = 10000 }
|
||||||
|
|
||||||
|
$itemUrl = ($baseUrl.TrimEnd('/') + '/item/' + $itemId)
|
||||||
|
$checkoutUrl = ($baseUrl.TrimEnd('/') + $checkoutPath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
|
||||||
|
Start-Process $itemUrl
|
||||||
|
Write-Log -Level Info -Message "Opened item modal URL for auto-order: $itemUrl" -Feature $FeatureName
|
||||||
|
Start-Sleep -Milliseconds $initialDelayMs
|
||||||
|
|
||||||
|
$shell = New-Object -ComObject WScript.Shell
|
||||||
|
$hostHint = $null
|
||||||
|
try { $hostHint = ([uri]$baseUrl).Host } catch {}
|
||||||
|
|
||||||
|
$titleCandidates = @(
|
||||||
|
'Bestel online bij YLO''S Kitchen Pittem',
|
||||||
|
'Bestel online bij YLO',
|
||||||
|
'Bestel online bij',
|
||||||
|
$windowTitleHint,
|
||||||
|
($windowTitleHint -replace "'", ''),
|
||||||
|
$hostHint,
|
||||||
|
'ylos-kitchen',
|
||||||
|
'unipage'
|
||||||
|
)
|
||||||
|
|
||||||
|
$activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs $activationTimeoutMs -RetryDelayMs 300
|
||||||
|
|
||||||
|
if (-not $activated) {
|
||||||
|
Write-Log -Level Warn -Message "Could not foreground browser window (title hint '$windowTitleHint'). Skipping key automation to avoid acting on the wrong window." -Feature $FeatureName
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($calibrationOnly) {
|
||||||
|
for ($i = 0; $i -lt $calibrationTabs; $i++) {
|
||||||
|
$shell.SendKeys('{TAB}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level Info -Message ("Calibration mode completed: sent {0} TAB keys and stopped." -f $calibrationTabs) -Feature $FeatureName
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Modal flow keyboard navigation (best-effort; tab indexes are configurable).
|
||||||
|
for ($i = 0; $i -lt $tabsToOption1; $i++) {
|
||||||
|
$shell.SendKeys('{TAB}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
}
|
||||||
|
$shell.SendKeys($toggleKey)
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $tabsBetweenOptions; $i++) {
|
||||||
|
$shell.SendKeys('{TAB}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
}
|
||||||
|
$shell.SendKeys($toggleKey)
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $tabsToAddButton; $i++) {
|
||||||
|
$shell.SendKeys('{TAB}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
}
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
Start-Sleep -Milliseconds 350
|
||||||
|
|
||||||
|
if ($useDirectCheckoutNavigation) {
|
||||||
|
Start-Process $checkoutUrl
|
||||||
|
Write-Log -Level Info -Message "Opened checkout URL directly: $checkoutUrl" -Feature $FeatureName
|
||||||
|
Start-Sleep -Milliseconds $checkoutOpenDelayMs
|
||||||
|
|
||||||
|
$activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs $activationTimeoutMs -RetryDelayMs 300
|
||||||
|
if (-not $activated) {
|
||||||
|
Write-Log -Level Warn -Message 'Could not foreground browser after opening checkout URL. Skipping remaining checkout automation.' -Feature $FeatureName
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Order overview: open item remark popup.
|
||||||
|
Start-Sleep -Milliseconds $checkoutOverviewRenderDelayMs
|
||||||
|
[void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200)
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToItemRemarkButton -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Item remark popup: type remark and confirm.
|
||||||
|
Start-Sleep -Milliseconds $itemRemarkPopupRenderDelayMs
|
||||||
|
[void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200)
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsBeforeItemRemarkInput -StepDelayMs $stepDelayMs
|
||||||
|
$itemRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultItemRemark'])
|
||||||
|
if ([string]::IsNullOrWhiteSpace($itemRemark)) { $itemRemark = 'zonder tomaat aub' }
|
||||||
|
$shell.SendKeys($itemRemark)
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToCloseItemRemarkPopup -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Back on order overview: move to order remark input, type text, confirm order.
|
||||||
|
Start-Sleep -Milliseconds $transitionDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToOrderRemarkInput -StepDelayMs $stepDelayMs
|
||||||
|
$orderRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultOrderRemark'])
|
||||||
|
if ([string]::IsNullOrWhiteSpace($orderRemark)) { $orderRemark = 'levering in Sioen Bistro' }
|
||||||
|
$shell.SendKeys($orderRemark)
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToFinalConfirmButton -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Payment option screen: move to default option and confirm, then handoff to user.
|
||||||
|
Start-Sleep -Milliseconds $transitionDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToPaymentOption -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
Write-Log -Level Info -Message 'Auto-order flow executed through direct checkout navigation and payment option confirmation; handoff to user at payment screen (best-effort).' -Feature $FeatureName
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($openCartPopup) {
|
||||||
|
if ($refreshBeforeCart) {
|
||||||
|
$shell.SendKeys('{F5}')
|
||||||
|
Write-Log -Level Info -Message 'Refreshing page to reset focus before tabbing to mini-cart.' -Feature $FeatureName
|
||||||
|
Start-Sleep -Milliseconds $postRefreshDelayMs
|
||||||
|
|
||||||
|
$activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs $activationTimeoutMs -RetryDelayMs 300
|
||||||
|
|
||||||
|
if (-not $activated) {
|
||||||
|
Write-Log -Level Warn -Message 'Could not re-foreground browser after refresh. Skipping cart popup.' -Feature $FeatureName
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $tabsToCartButton; $i++) {
|
||||||
|
$shell.SendKeys('{TAB}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
}
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $tabsToConfirmButton; $i++) {
|
||||||
|
$shell.SendKeys('{TAB}')
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
}
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Delivery popup: re-activate and wait longer before 4-tab date/time confirm.
|
||||||
|
Start-Sleep -Milliseconds $deliveryPopupRenderDelayMs
|
||||||
|
[void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200)
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToDateTimeConfirm -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Order overview: open item remark popup.
|
||||||
|
Start-Sleep -Milliseconds $checkoutOverviewRenderDelayMs
|
||||||
|
[void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200)
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToItemRemarkButton -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Item remark popup: type remark and confirm.
|
||||||
|
Start-Sleep -Milliseconds $itemRemarkPopupRenderDelayMs
|
||||||
|
[void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200)
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsBeforeItemRemarkInput -StepDelayMs $stepDelayMs
|
||||||
|
$itemRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultItemRemark'])
|
||||||
|
if ([string]::IsNullOrWhiteSpace($itemRemark)) { $itemRemark = 'zonder tomaat aub' }
|
||||||
|
$shell.SendKeys($itemRemark)
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToCloseItemRemarkPopup -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Back on order overview: move to order remark input, type text, confirm order.
|
||||||
|
Start-Sleep -Milliseconds $transitionDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToOrderRemarkInput -StepDelayMs $stepDelayMs
|
||||||
|
$orderRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultOrderRemark'])
|
||||||
|
if ([string]::IsNullOrWhiteSpace($orderRemark)) { $orderRemark = 'levering in Sioen Bistro' }
|
||||||
|
$shell.SendKeys($orderRemark)
|
||||||
|
Start-Sleep -Milliseconds $stepDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToFinalConfirmButton -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
|
||||||
|
# Payment option screen: move to default option and confirm, then handoff to user.
|
||||||
|
Start-Sleep -Milliseconds $transitionDelayMs
|
||||||
|
Send-TabSteps -Shell $shell -Count $tabsToPaymentOption -StepDelayMs $stepDelayMs
|
||||||
|
$shell.SendKeys('{ENTER}')
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level Info -Message 'Auto-order flow executed through payment option confirmation; handoff to user at payment screen (best-effort).' -Feature $FeatureName
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Log -Level Error -Message "Auto-order flow failed: $_" -Feature $FeatureName
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-Feature {
|
function Invoke-Feature {
|
||||||
param(
|
param(
|
||||||
[hashtable]$Config,
|
[hashtable]$Config,
|
||||||
|
|||||||
@@ -127,6 +127,17 @@ function Invoke-Feature {
|
|||||||
|
|
||||||
if ($autoOrderConfig -and $autoOrderConfig['enabled']) {
|
if ($autoOrderConfig -and $autoOrderConfig['enabled']) {
|
||||||
Write-Log -Level Info -Message 'SandwichReminder-AutoOrder is enabled; running auto-order flow.' -Feature 'SandwichReminder'
|
Write-Log -Level Info -Message 'SandwichReminder-AutoOrder is enabled; running auto-order flow.' -Feature 'SandwichReminder'
|
||||||
|
|
||||||
|
if (-not (Get-Command -Name Invoke-SandwichAutoOrderFlow -ErrorAction SilentlyContinue)) {
|
||||||
|
$autoOrderFeaturePath = Join-Path $PSScriptRoot 'SandwichReminder-AutoOrder.ps1'
|
||||||
|
if (Test-Path $autoOrderFeaturePath) {
|
||||||
|
. $autoOrderFeaturePath
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "Auto-order feature module not found at '$autoOrderFeaturePath'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[void](Invoke-SandwichAutoOrderFlow -AutoOrderConfig $autoOrderConfig -FallbackUrl $Config['url'])
|
[void](Invoke-SandwichAutoOrderFlow -AutoOrderConfig $autoOrderConfig -FallbackUrl $Config['url'])
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
# SandwichAutoOrder.ps1 — best-effort browser keyboard automation for personal sandwich flow.
|
|
||||||
|
|
||||||
function Invoke-AppActivateBestEffort {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Shell,
|
|
||||||
[Parameter(Mandatory)][string[]]$TitleCandidates,
|
|
||||||
[int]$TimeoutMs = 10000,
|
|
||||||
[int]$RetryDelayMs = 300
|
|
||||||
)
|
|
||||||
|
|
||||||
$deadline = (Get-Date).AddMilliseconds($TimeoutMs)
|
|
||||||
$candidates = @($TitleCandidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
||||||
|
|
||||||
do {
|
|
||||||
foreach ($candidate in $candidates) {
|
|
||||||
if ($Shell.AppActivate($candidate)) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Milliseconds $RetryDelayMs
|
|
||||||
} while ((Get-Date) -lt $deadline)
|
|
||||||
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-SandwichAutoOrderFlow {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][hashtable]$AutoOrderConfig,
|
|
||||||
[Parameter(Mandatory)][string]$FallbackUrl,
|
|
||||||
[string]$FeatureName = 'SandwichReminder-AutoOrder'
|
|
||||||
)
|
|
||||||
|
|
||||||
$baseUrl = [string]$AutoOrderConfig['baseUrl']
|
|
||||||
if ([string]::IsNullOrWhiteSpace($baseUrl)) { $baseUrl = 'https://ylos-kitchen.unipage.eu' }
|
|
||||||
|
|
||||||
$itemId = [string]$AutoOrderConfig['itemId']
|
|
||||||
if ([string]::IsNullOrWhiteSpace($itemId)) { $itemId = 'dmwmo3' }
|
|
||||||
|
|
||||||
$windowTitleHint = [string]$AutoOrderConfig['windowTitleHint']
|
|
||||||
if ([string]::IsNullOrWhiteSpace($windowTitleHint)) { $windowTitleHint = "YLO'S Kitchen" }
|
|
||||||
|
|
||||||
$initialDelayMs = [int]$AutoOrderConfig['initialDelayMs']
|
|
||||||
if ($initialDelayMs -le 0) { $initialDelayMs = 2500 }
|
|
||||||
|
|
||||||
$stepDelayMs = [int]$AutoOrderConfig['stepDelayMs']
|
|
||||||
if ($stepDelayMs -le 0) { $stepDelayMs = 120 }
|
|
||||||
|
|
||||||
$toggleKeySetting = [string]$AutoOrderConfig['optionToggleKey']
|
|
||||||
if ([string]::IsNullOrWhiteSpace($toggleKeySetting)) { $toggleKeySetting = 'SPACE' }
|
|
||||||
$toggleKey = if ($toggleKeySetting.ToUpperInvariant() -eq 'ENTER') { '{ENTER}' } else { ' ' }
|
|
||||||
|
|
||||||
$calibrationOnly = [bool]$AutoOrderConfig['calibrationOnly']
|
|
||||||
$calibrationTabs = [int]$AutoOrderConfig['calibrationTabs']
|
|
||||||
if ($calibrationTabs -lt 0) { $calibrationTabs = 0 }
|
|
||||||
|
|
||||||
$tabsToOption1 = [int]$AutoOrderConfig['tabsToOption1']
|
|
||||||
if ($tabsToOption1 -lt 0) { $tabsToOption1 = 3 }
|
|
||||||
|
|
||||||
$tabsBetweenOptions = [int]$AutoOrderConfig['tabsBetweenOptions']
|
|
||||||
if ($tabsBetweenOptions -lt 0) { $tabsBetweenOptions = 2 }
|
|
||||||
|
|
||||||
$tabsToAddButton = [int]$AutoOrderConfig['tabsToAddButton']
|
|
||||||
if ($tabsToAddButton -lt 0) { $tabsToAddButton = 4 }
|
|
||||||
|
|
||||||
$openCartPopup = [bool]$AutoOrderConfig['openCartPopupAfterAdd']
|
|
||||||
$refreshBeforeCart = [bool]$AutoOrderConfig['refreshBeforeCart']
|
|
||||||
$postRefreshDelayMs = [int]$AutoOrderConfig['postRefreshDelayMs']
|
|
||||||
if ($postRefreshDelayMs -le 0) { $postRefreshDelayMs = 2500 }
|
|
||||||
$tabsToCartButton = [int]$AutoOrderConfig['tabsToCartButton']
|
|
||||||
if ($tabsToCartButton -le 0) { $tabsToCartButton = 5 }
|
|
||||||
$tabsToConfirmButton = [int]$AutoOrderConfig['tabsToConfirmButton']
|
|
||||||
if ($tabsToConfirmButton -le 0) { $tabsToConfirmButton = 3 }
|
|
||||||
|
|
||||||
$activationTimeoutMs = [int]$AutoOrderConfig['activationTimeoutMs']
|
|
||||||
if ($activationTimeoutMs -le 0) { $activationTimeoutMs = 10000 }
|
|
||||||
|
|
||||||
$itemUrl = ($baseUrl.TrimEnd('/') + '/item/' + $itemId)
|
|
||||||
|
|
||||||
try {
|
|
||||||
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
|
|
||||||
|
|
||||||
Start-Process $itemUrl
|
|
||||||
Write-Log -Level Info -Message "Opened item modal URL for auto-order: $itemUrl" -Feature $FeatureName
|
|
||||||
Start-Sleep -Milliseconds $initialDelayMs
|
|
||||||
|
|
||||||
$shell = New-Object -ComObject WScript.Shell
|
|
||||||
$hostHint = $null
|
|
||||||
try { $hostHint = ([uri]$baseUrl).Host } catch {}
|
|
||||||
|
|
||||||
$activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates @(
|
|
||||||
'Bestel online bij YLO''S Kitchen Pittem',
|
|
||||||
'Bestel online bij YLO',
|
|
||||||
'Bestel online bij',
|
|
||||||
$windowTitleHint,
|
|
||||||
($windowTitleHint -replace "'", ''),
|
|
||||||
$hostHint,
|
|
||||||
'ylos-kitchen',
|
|
||||||
'unipage'
|
|
||||||
) -TimeoutMs $activationTimeoutMs -RetryDelayMs 300
|
|
||||||
|
|
||||||
if (-not $activated) {
|
|
||||||
Write-Log -Level Warn -Message "Could not foreground browser window (title hint '$windowTitleHint'). Skipping key automation to avoid acting on the wrong window." -Feature $FeatureName
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($calibrationOnly) {
|
|
||||||
for ($i = 0; $i -lt $calibrationTabs; $i++) {
|
|
||||||
$shell.SendKeys('{TAB}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level Info -Message ("Calibration mode completed: sent {0} TAB keys and stopped." -f $calibrationTabs) -Feature $FeatureName
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Modal flow keyboard navigation (best-effort; tab indexes are configurable).
|
|
||||||
for ($i = 0; $i -lt $tabsToOption1; $i++) {
|
|
||||||
$shell.SendKeys('{TAB}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
}
|
|
||||||
$shell.SendKeys($toggleKey)
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
|
|
||||||
for ($i = 0; $i -lt $tabsBetweenOptions; $i++) {
|
|
||||||
$shell.SendKeys('{TAB}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
}
|
|
||||||
$shell.SendKeys($toggleKey)
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
|
|
||||||
for ($i = 0; $i -lt $tabsToAddButton; $i++) {
|
|
||||||
$shell.SendKeys('{TAB}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
}
|
|
||||||
$shell.SendKeys('{ENTER}')
|
|
||||||
Start-Sleep -Milliseconds 350
|
|
||||||
|
|
||||||
if ($openCartPopup) {
|
|
||||||
if ($refreshBeforeCart) {
|
|
||||||
$shell.SendKeys('{F5}')
|
|
||||||
Write-Log -Level Info -Message 'Refreshing page to reset focus before tabbing to mini-cart.' -Feature $FeatureName
|
|
||||||
Start-Sleep -Milliseconds $postRefreshDelayMs
|
|
||||||
|
|
||||||
$activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates @(
|
|
||||||
'Bestel online bij YLO''S Kitchen Pittem',
|
|
||||||
'Bestel online bij YLO',
|
|
||||||
'Bestel online bij',
|
|
||||||
$windowTitleHint,
|
|
||||||
($windowTitleHint -replace "'", ''),
|
|
||||||
$hostHint,
|
|
||||||
'ylos-kitchen',
|
|
||||||
'unipage'
|
|
||||||
) -TimeoutMs $activationTimeoutMs -RetryDelayMs 300
|
|
||||||
|
|
||||||
if (-not $activated) {
|
|
||||||
Write-Log -Level Warn -Message 'Could not re-foreground browser after refresh. Skipping cart popup.' -Feature $FeatureName
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($i = 0; $i -lt $tabsToCartButton; $i++) {
|
|
||||||
$shell.SendKeys('{TAB}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
}
|
|
||||||
$shell.SendKeys('{ENTER}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
|
|
||||||
for ($i = 0; $i -lt $tabsToConfirmButton; $i++) {
|
|
||||||
$shell.SendKeys('{TAB}')
|
|
||||||
Start-Sleep -Milliseconds $stepDelayMs
|
|
||||||
}
|
|
||||||
$shell.SendKeys('{ENTER}')
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level Info -Message 'Auto-order flow executed up to add-to-cart and cart popup open (best-effort).' -Feature $FeatureName
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level Error -Message "Auto-order flow failed: $_" -Feature $FeatureName
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
' runner-launcher.vbs
|
||||||
|
' Starts runner.ps1 without a visible console window.
|
||||||
|
' Uses a shown/minimized style (not 0) so the process joins the interactive window station,
|
||||||
|
' which is required for WinForms dialogs to appear. PowerShell's own
|
||||||
|
' -WindowStyle Hidden suppresses the console window.
|
||||||
|
|
||||||
|
Option Explicit
|
||||||
|
|
||||||
|
If WScript.Arguments.Count < 1 Then
|
||||||
|
WScript.Quit 1
|
||||||
|
End If
|
||||||
|
|
||||||
|
Dim runnerPath
|
||||||
|
runnerPath = WScript.Arguments(0)
|
||||||
|
|
||||||
|
Dim shell, cmd
|
||||||
|
Set shell = CreateObject("WScript.Shell")
|
||||||
|
cmd = "powershell.exe -NoLogo -NoProfile -Sta -WindowStyle Hidden -ExecutionPolicy Bypass -File """ & runnerPath & """"
|
||||||
|
|
||||||
|
' Window style 1 = SW_SHOWNORMAL — do NOT use 0 here.
|
||||||
|
' Style 0 (CREATE_NO_WINDOW) detaches the process from the interactive
|
||||||
|
' window station, which prevents WinForms dialogs from rendering.
|
||||||
|
' PowerShell's own -WindowStyle Hidden flag suppresses the console window.
|
||||||
|
shell.Run cmd, 1, False
|
||||||
|
WScript.Quit 0
|
||||||
+22
-1
@@ -11,7 +11,6 @@ $script:InternalRoot = $PSScriptRoot # runner.ps1 lives in internal\
|
|||||||
. (Join-Path $InternalRoot 'lib\NetworkUtils.ps1')
|
. (Join-Path $InternalRoot 'lib\NetworkUtils.ps1')
|
||||||
. (Join-Path $InternalRoot 'lib\ToastHelper.ps1')
|
. (Join-Path $InternalRoot 'lib\ToastHelper.ps1')
|
||||||
. (Join-Path $InternalRoot 'lib\PromptHelper.ps1')
|
. (Join-Path $InternalRoot 'lib\PromptHelper.ps1')
|
||||||
. (Join-Path $InternalRoot 'lib\SandwichAutoOrder.ps1')
|
|
||||||
. (Join-Path $InternalRoot 'lib\Config.ps1')
|
. (Join-Path $InternalRoot 'lib\Config.ps1')
|
||||||
|
|
||||||
Write-Log -Level Info -Message '────── Runner started ──────' -Feature 'Runner'
|
Write-Log -Level Info -Message '────── Runner started ──────' -Feature 'Runner'
|
||||||
@@ -38,7 +37,23 @@ foreach ($featureKey in $config.features.Keys) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Per-feature mutex: prevents two concurrent runner instances from executing
|
||||||
|
# the same feature simultaneously (e.g. if the previous run is still in progress).
|
||||||
|
$mutexName = "Global\PSAutomation-Feature-$featureKey"
|
||||||
|
$mutex = $null
|
||||||
|
$mutexAcquired = $false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$mutex = New-Object System.Threading.Mutex($false, $mutexName)
|
||||||
|
$mutexAcquired = $mutex.WaitOne(0) # non-blocking: skip if already locked
|
||||||
|
|
||||||
|
if (-not $mutexAcquired) {
|
||||||
|
Write-Log -Level Warn `
|
||||||
|
-Message "Feature '$featureKey' skipped: another instance is still running." `
|
||||||
|
-Feature 'Runner'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
# Dot-source the feature — defines $FeatureMeta and Invoke-Feature in local scope.
|
# Dot-source the feature — defines $FeatureMeta and Invoke-Feature in local scope.
|
||||||
# Each iteration overwrites Invoke-Feature, which is fine because we call it immediately.
|
# Each iteration overwrites Invoke-Feature, which is fine because we call it immediately.
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -61,6 +76,12 @@ foreach ($featureKey in $config.features.Keys) {
|
|||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if ($mutexAcquired -and $null -ne $mutex) {
|
||||||
|
try { $mutex.ReleaseMutex() } catch {}
|
||||||
|
}
|
||||||
|
if ($null -ne $mutex) {
|
||||||
|
try { $mutex.Dispose() } catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user