security+robustness(py/gui): thread-safety, upload hardening, keyring

macro_manager:
- All macro reads/writes guarded by a lock; macros.json written atomically
  (temp + fsync + os.replace). Commands are copied under the lock and executed
  outside it so wait/keyboard steps don't block other threads.
- last_used disk writes debounced (>=5s) instead of a full rewrite per press.
- Validate key/hotkey names against pyautogui.KEYBOARD_KEYS and clamp wait ms;
  invalid input is skipped/logged, not blindly executed.

web_server:
- WebSocket receive loop always disconnects (fixes dead-socket leak); broadcast
  prunes failed sockets.
- POST /api/execute offloaded to a thread executor so a long macro no longer
  blocks the event loop.
- Image upload hardened: 5MB cap (413), real-image validation via Pillow,
  server-generated name under macro_images/, returns a relative path only,
  cleans up on failure.

gui:
- Execute macros off the Qt main thread (signal back to status bar); debounce
  resizeEvent and only rebuild when the column count changes.
- Relay password stored in the OS keyring when available (plaintext JSON blanked;
  graceful fallback if no backend); delete dialogs name the macro; honor the
  minimize-to-tray setting; surface save failures.
- Add media/system keys (volume, play/pause, track nav) to the macro editor.
- THEME gains shared danger/accent-hover colors; drop dead imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 10:26:22 -07:00
parent 435cc1aca9
commit beba425868
7 changed files with 402 additions and 117 deletions
+9 -3
View File
@@ -123,7 +123,7 @@ class SettingsDialog(QDialog):
color: white;
}}
QPushButton:hover {{
background-color: #0096ff;
background-color: {THEME['accent_hover']};
}}
""")
save_btn.clicked.connect(self.save_settings)
@@ -316,9 +316,15 @@ class SettingsDialog(QDialog):
self.settings_manager.set('relay.enabled', new_enabled)
self.settings_manager.set('relay.server_url', new_url)
self.settings_manager.set('relay.password', new_password)
self.settings_manager.save()
# Store the password via the keyring-aware setter (also persists the
# rest of the settings). Avoid writing the plaintext password to JSON.
ok = self.settings_manager.set_relay_password(new_password)
if not ok:
QMessageBox.warning(
self, "Save Warning",
"Settings could not be fully saved to disk."
)
if relay_changed:
self.relay_settings_changed.emit()