Add new features: DefaultBrowser, DynamicLock, SandwichReminder

- Implemented DefaultBrowser feature to notify users when the default browser does not match the configured app.
- Added DynamicLock feature to disable Dynamic Lock while connected to a specific network and re-enable it after disconnecting.
- Created SandwichReminder feature to prompt users to order a sandwich during work hours based on network and time settings.

Introduced helper libraries for configuration, elevation, logging, network utilities, and toast notifications.

- Config.ps1: Added functions for reading and writing configuration and state files.
- Elevation.ps1: Added functions to check for administrator privileges and request elevation.
- Logging.ps1: Implemented a shared logging utility for consistent logging across features.
- NetworkUtils.ps1: Added a function to check for DNS suffix connectivity.
- ToastHelper.ps1: Created a helper for displaying Windows toast notifications.

Implemented runner.ps1 as the main entry point for executing features based on configuration.
This commit is contained in:
Arne Moerman
2026-05-08 11:48:39 +02:00
commit 34ea1eb4b2
12 changed files with 1771 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# ── Feature metadata ──────────────────────────────────────────────────────────
$FeatureMeta = @{
Name = 'DefaultBrowser'
Description = 'Notify when the default browser does not match the configured app'
Settings = @(
@{
Key = 'targetProgId'
Label = 'Target ProgId'
Type = 'string'
Default = 'OperaGXStable'
Description = 'ProgId of the desired default browser (e.g. OperaGXStable, ChromeHTML, FirefoxURL-308046B0AF4A39CB)'
}
)
}
# ── Feature implementation ────────────────────────────────────────────────────
function Invoke-Feature {
param(
[hashtable]$Config,
[hashtable]$State
)
if (-not $State) { $State = @{} }
if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null }
$today = (Get-Date).ToString('yyyy-MM-dd')
# Only notify once per day
if ($State['lastShownDate'] -eq $today) {
Write-Log -Level Info -Message 'Already checked today, skipping.' -Feature 'DefaultBrowser'
return $State
}
$regPath = 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice'
try {
$currentProgId = (Get-ItemProperty -Path $regPath -Name 'ProgId' -ErrorAction Stop).ProgId
}
catch {
Write-Log -Level Warn `
-Message "Could not read current default browser ProgId from registry: $_" `
-Feature 'DefaultBrowser'
return $State
}
if ($currentProgId -eq $Config['targetProgId']) {
Write-Log -Level Info `
-Message "Default browser OK: '$currentProgId'." `
-Feature 'DefaultBrowser'
$State['lastShownDate'] = $today
return $State
}
Write-Log -Level Warn `
-Message "Default browser mismatch — found '$currentProgId', expected '$($Config['targetProgId'])'." `
-Feature 'DefaultBrowser'
# Note: Windows 11 blocks programmatic default-browser changes via registry hash protection.
# We guide the user to the Settings page instead.
Show-ToastNotification `
-Title 'Default Browser' `
-Body ("Default browser is '$currentProgId'. Click below to set it to $($Config['targetProgId']).") `
-Buttons @(
@{ Label = 'Open Default Apps Settings'; Action = 'ms-settings:defaultapps' },
@{ Label = 'Dismiss'; Action = 'dismiss' }
)
$State['lastShownDate'] = $today
return $State
}
+99
View File
@@ -0,0 +1,99 @@
# ── Feature metadata ──────────────────────────────────────────────────────────
# $FeatureMeta is read by configure.ps1 for discovery, display, and config UI.
# Invoke-Feature is called by runner.ps1 on each scheduled cycle.
$FeatureMeta = @{
Name = 'DynamicLock'
Description = 'Disable Dynamic Lock while on a specific network; re-enable after disconnecting'
Settings = @(
@{
Key = 'network'
Label = 'DNS Suffix'
Type = 'string'
Default = 'h.arnemoerman.be'
Description = 'Connection-specific DNS suffix of the target network (e.g. h.arnemoerman.be)'
},
@{
Key = 'revertAfterMinutes'
Label = 'Revert After (minutes)'
Type = 'int'
Default = 10
Description = 'Re-enable Dynamic Lock after being disconnected for this many minutes'
}
)
}
# ── Feature implementation ────────────────────────────────────────────────────
function Invoke-Feature {
param(
[hashtable]$Config,
[hashtable]$State
)
if (-not $State) { $State = @{} }
if (-not $State.ContainsKey('isConnected')) { $State['isConnected'] = $false }
if (-not $State.ContainsKey('lastConnectedTime')) { $State['lastConnectedTime'] = $null }
if (-not $State.ContainsKey('dynamicLockDisabled')) { $State['dynamicLockDisabled'] = $false }
$regPath = 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
$isConnected = Test-DnsSuffixConnected -Suffix $Config['network']
if ($isConnected) {
$State['isConnected'] = $true
$State['lastConnectedTime'] = (Get-Date).ToString('o')
if (-not $State['dynamicLockDisabled']) {
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
Set-ItemProperty -Path $regPath -Name 'EnableGoodbye' -Value 0 -Type DWord
$State['dynamicLockDisabled'] = $true
Write-Log -Level Info `
-Message "Dynamic Lock disabled (connected to '$($Config['network'])')" `
-Feature 'DynamicLock'
}
}
else {
$State['isConnected'] = $false
# If we disabled Dynamic Lock earlier, check whether we've been away long enough to re-enable
if ($State['dynamicLockDisabled'] -and $null -ne $State['lastConnectedTime']) {
try {
$lastConnected = [datetime]::Parse($State['lastConnectedTime'])
$minutesGone = ((Get-Date) - $lastConnected).TotalMinutes
if ($minutesGone -ge [double]$Config['revertAfterMinutes']) {
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
Set-ItemProperty -Path $regPath -Name 'EnableGoodbye' -Value 1 -Type DWord
$State['dynamicLockDisabled'] = $false
Write-Log -Level Info `
-Message ("Dynamic Lock re-enabled (disconnected for {0} min, threshold {1} min)" -f
[int]$minutesGone, $Config['revertAfterMinutes']) `
-Feature 'DynamicLock'
}
else {
Write-Log -Level Info `
-Message ("Waiting to re-enable Dynamic Lock ({0}/{1} min elapsed)" -f
[int]$minutesGone, $Config['revertAfterMinutes']) `
-Feature 'DynamicLock'
}
}
catch {
Write-Log -Level Warn `
-Message "Could not parse lastConnectedTime '$($State['lastConnectedTime'])': $_" `
-Feature 'DynamicLock'
}
}
else {
Write-Log -Level Info `
-Message "Not connected to '$($Config['network'])'; Dynamic Lock state unchanged." `
-Feature 'DynamicLock'
}
}
return $State
}
+102
View File
@@ -0,0 +1,102 @@
# ── Feature metadata ──────────────────────────────────────────────────────────
$FeatureMeta = @{
Name = 'SandwichReminder'
Description = 'Ask if you want to order a sandwich when at work at the configured time'
Settings = @(
@{
Key = 'network'
Label = 'Work Network DNS Suffix'
Type = 'string'
Default = 'sioen.grp'
Description = 'DNS suffix that identifies the work network'
},
@{
Key = 'reminderTime'
Label = 'Reminder Time (HH:mm)'
Type = 'string'
Default = '09:00'
Description = 'Time to show the reminder in 24-hour format (e.g. 09:00)'
},
@{
Key = 'windowMinutes'
Label = 'Time Window (minutes)'
Type = 'int'
Default = 5
Description = 'Show the reminder if the runner fires within this many minutes of the reminder time'
},
@{
Key = 'url'
Label = 'Order URL'
Type = 'string'
Default = 'https://ylos-kitchen.unipage.eu'
Description = 'URL to open when clicking Yes'
}
)
}
# ── Feature implementation ────────────────────────────────────────────────────
function Invoke-Feature {
param(
[hashtable]$Config,
[hashtable]$State
)
if (-not $State) { $State = @{} }
if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null }
$today = (Get-Date).ToString('yyyy-MM-dd')
# Only show once per day
if ($State['lastShownDate'] -eq $today) {
Write-Log -Level Info -Message 'Already shown today, skipping.' -Feature 'SandwichReminder'
return $State
}
# Parse reminder time
try {
$targetTime = [datetime]::ParseExact($Config['reminderTime'], 'HH:mm', $null)
}
catch {
Write-Log -Level Error `
-Message "Invalid reminderTime format '$($Config['reminderTime'])' — expected HH:mm: $_" `
-Feature 'SandwichReminder'
return $State
}
# Check whether we are within the configured time window
$now = Get-Date
$todayTarget = Get-Date -Hour $targetTime.Hour -Minute $targetTime.Minute -Second 0
$diffMinutes = [Math]::Abs(($now - $todayTarget).TotalMinutes)
$window = [int]$Config['windowMinutes']
if ($diffMinutes -gt $window) {
Write-Log -Level Info `
-Message ("Outside time window (diff: {0:F1} min, window: ±{1} min), skipping." -f $diffMinutes, $window) `
-Feature 'SandwichReminder'
return $State
}
# Check network
if (-not (Test-DnsSuffixConnected -Suffix $Config['network'])) {
Write-Log -Level Info `
-Message "Not connected to work network ('$($Config['network'])'), skipping." `
-Feature 'SandwichReminder'
return $State
}
Write-Log -Level Info -Message 'Showing sandwich reminder toast.' -Feature 'SandwichReminder'
Show-ToastNotification `
-Title 'Sandwich Order' `
-Body 'Do you want to order a sandwich today?' `
-Buttons @(
@{ Label = 'Yes, order now!'; Action = $Config['url'] },
@{ Label = 'No thanks'; Action = 'dismiss' }
)
$State['lastShownDate'] = $today
return $State
}