Hi — first, thanks for the project, the concept is great.
While getting it running on macOS 26.3.1 with an MX Master 4 I hit four issues that I have local fixes for and would like to open a PR for, per the contributing guide.
Some overlap with #2 (where multiple users report "no haptics" on Tahoe even after the Smart Action is configured). Not related to #4 (notification watcher), which already has a separate fix proposed.
1. Browser extension is silently blocked by Chrome (CORS / PNA)
WebServer.swift returns a flat HTTP/1.1 200 OK with no CORS headers and doesn't handle the OPTIONS preflight. Modern Chrome:
- Sends a preflight for any cross-origin POST from a
chrome-extension:// origin to localhost.
- Requires
Access-Control-Allow-Private-Network: true (PNA, Chrome 130+) for any request to a private network.
- Rejects
Access-Control-Allow-Origin: * when credentials (cookies) are attached, which Chrome does for the chrome-extension → localhost case.
Net effect: extension popup shows "Connected" (because the GET succeeds), but no POST from the test buttons or content script ever reaches the server. Confirmed by sniffing the loopback — zero traffic. After adding preflight handling that echoes the request Origin and sets Allow-Credentials: true + Allow-Private-Network: true, the extension works.
2. @AppStorage defaults are never written to UserDefaults
ContentView declares:
@AppStorage("allowWebHaptics") private var allowWebHaptics: Bool = true
@AppStorage("systemNotificationsEnabled") private var systemNotificationsEnabled: Bool = true
But WebServer.processJson and NotificationWatcher read these via UserDefaults.standard.bool(forKey:), which returns false when the key is absent. @AppStorage doesn't actually write the default — it only writes when the binding is set.
Net effect: on a fresh install, both web haptics and notification haptics are silently off until the user toggles each switch in the UI. Easy fix: register defaults on launch via UserDefaults.standard.register(defaults:).
3. AppleScript keystroke path is unreliable on ad-hoc signed builds, macOS 14+
HapticEngine.trigger() uses NSAppleScript → tell application "System Events" to key code 32 …, which requires the separate kTCCServiceAppleEvents Automation permission for "System Events" (in addition to Accessibility).
On macOS 14+ with ad-hoc signed builds (which build_with_icon.sh produces, since most users don't have a Developer ID), the Automation permission prompt is often suppressed due to cdhash validation failures (visible in log show as SecStaticCodeCheckValidity → status: -67050). The app silently does nothing and the user has no UI feedback explaining why.
Replacing the AppleScript path with CGEvent.post(tap: .cghidEventTap):
- Uses only the existing Accessibility permission, no Automation prompt needed.
- Bypasses System Events entirely.
- Is functionally indistinguishable to Logi Options+'s Smart Action listener — verified to fire on Tahoe 26.3.1.
This likely explains several "no haptics" reports in #2 where users had Accessibility granted and the Smart Action mapped correctly but still saw nothing.
4. Port 3000 collides with common dev servers
Hardcoded port 3000 collides with Next.js, Rails, Flask defaults, etc. When occupied, WebServer.start() swallows the bind error and the server silently fails to come up — the UI status stays at "Initializing…" forever.
Proposed fix:
- Default port changed to
3030.
- Port becomes configurable via the UI (added a TextField in the Web Browser card, persisted via
@AppStorage("webServerPort")).
- Bind failures now surface as "Port X in use" in the status row.
The browser extension (background.js/popup.js/manifest.json) currently hardcodes the port too — bumped its default to match. Making the extension port configurable as well could be a follow-up.
Bonus: build script not idempotent
mv build/"Haptic Master.app" . fails when a prior build exists, leaving the freshly built bundle in build/ and silently deploying the stale one (no error, since echo "✅ Build Complete!" runs unconditionally after). Trivial to fix by rm -rf the destination first or set -e in the script.
Note on rebuilds and cdhash
Worth documenting somewhere: each rebuild via build_with_icon.sh produces a new cdhash (ad-hoc signature changes with the binary). macOS appears to invalidate the prior Accessibility grant when the cdhash changes, requiring a re-grant via System Settings on every rebuild during development. Not something this PR addresses, but a footgun for any contributor.
I'll open a draft PR with fixes for (1) + (2) + (3) + (4) at once, since they're what makes the project work out of the box on a fresh macOS install. Happy to split, scope down, or change approach based on your guidance.
Tested locally on macOS 26.3.1 with MX Master 4 + Logi Options+ 2.x.
Hi — first, thanks for the project, the concept is great.
While getting it running on macOS 26.3.1 with an MX Master 4 I hit four issues that I have local fixes for and would like to open a PR for, per the contributing guide.
Some overlap with #2 (where multiple users report "no haptics" on Tahoe even after the Smart Action is configured). Not related to #4 (notification watcher), which already has a separate fix proposed.
1. Browser extension is silently blocked by Chrome (CORS / PNA)
WebServer.swiftreturns a flatHTTP/1.1 200 OKwith no CORS headers and doesn't handle theOPTIONSpreflight. Modern Chrome:chrome-extension://origin tolocalhost.Access-Control-Allow-Private-Network: true(PNA, Chrome 130+) for any request to a private network.Access-Control-Allow-Origin: *when credentials (cookies) are attached, which Chrome does for the chrome-extension → localhost case.Net effect: extension popup shows "Connected" (because the GET succeeds), but no POST from the test buttons or content script ever reaches the server. Confirmed by sniffing the loopback — zero traffic. After adding preflight handling that echoes the request Origin and sets
Allow-Credentials: true+Allow-Private-Network: true, the extension works.2.
@AppStoragedefaults are never written toUserDefaultsContentViewdeclares:But
WebServer.processJsonandNotificationWatcherread these viaUserDefaults.standard.bool(forKey:), which returnsfalsewhen the key is absent.@AppStoragedoesn't actually write the default — it only writes when the binding is set.Net effect: on a fresh install, both web haptics and notification haptics are silently off until the user toggles each switch in the UI. Easy fix: register defaults on launch via
UserDefaults.standard.register(defaults:).3. AppleScript keystroke path is unreliable on ad-hoc signed builds, macOS 14+
HapticEngine.trigger()usesNSAppleScript→tell application "System Events" to key code 32 …, which requires the separatekTCCServiceAppleEventsAutomation permission for "System Events" (in addition to Accessibility).On macOS 14+ with ad-hoc signed builds (which
build_with_icon.shproduces, since most users don't have a Developer ID), the Automation permission prompt is often suppressed due to cdhash validation failures (visible inlog showasSecStaticCodeCheckValidity→status: -67050). The app silently does nothing and the user has no UI feedback explaining why.Replacing the AppleScript path with
CGEvent.post(tap: .cghidEventTap):This likely explains several "no haptics" reports in #2 where users had Accessibility granted and the Smart Action mapped correctly but still saw nothing.
4. Port 3000 collides with common dev servers
Hardcoded port
3000collides with Next.js, Rails, Flask defaults, etc. When occupied,WebServer.start()swallows the bind error and the server silently fails to come up — the UI status stays at "Initializing…" forever.Proposed fix:
3030.@AppStorage("webServerPort")).The browser extension (
background.js/popup.js/manifest.json) currently hardcodes the port too — bumped its default to match. Making the extension port configurable as well could be a follow-up.Bonus: build script not idempotent
mv build/"Haptic Master.app" .fails when a prior build exists, leaving the freshly built bundle inbuild/and silently deploying the stale one (no error, sinceecho "✅ Build Complete!"runs unconditionally after). Trivial to fix byrm -rfthe destination first orset -ein the script.Note on rebuilds and cdhash
Worth documenting somewhere: each rebuild via
build_with_icon.shproduces a new cdhash (ad-hoc signature changes with the binary). macOS appears to invalidate the prior Accessibility grant when the cdhash changes, requiring a re-grant via System Settings on every rebuild during development. Not something this PR addresses, but a footgun for any contributor.I'll open a draft PR with fixes for (1) + (2) + (3) + (4) at once, since they're what makes the project work out of the box on a fresh macOS install. Happy to split, scope down, or change approach based on your guidance.
Tested locally on macOS 26.3.1 with MX Master 4 + Logi Options+ 2.x.