Background Workers

How Hippopotamoose keeps the UI responsive while running long operations.

Why QThreads?

PyQt6 UI code runs on a single thread — the main thread. Any blocking operation (network call, file write, subprocess wait) run directly on the main thread will freeze the entire UI until it completes. All long-running work in Hippopotamoose runs in QThread subclasses so the UI stays responsive.

Common Worker Pattern

Every worker follows the same structure:

class XxxWorker(QThread):
    finished = pyqtSignal(bool, str)   # success + result
    status   = pyqtSignal(str)         # progress message

    def __init__(self, ...):
        super().__init__()
        # store parameters

    def run(self):                     # called when thread starts
        try:
            # ... do blocking work ...
            self.status.emit("Working...")
            self.finished.emit(True, result)
        except Exception as e:
            self.finished.emit(False, str(e))

Signals emitted from the worker thread are automatically queued and delivered to connected slots on the main thread by Qt's event loop — no manual locking required.

Worker Reference

WebCopyWorker

File: core/webcopy_worker.py

Purpose: Downloads an entire website using Cyotek WebCopy.

SignalParametersMeaning
finished(bool, slug, error_msg)Download complete. bool = success.
status(str)Current status message.
progress(int done, int total)File count progress.

Timeout: 5 minutes hard limit. Process killed if exceeded.

Progress parsing: Reads wcopy.exe stdout and extracts [done/total] patterns.

WebCopyQueue

File: core/webcopy_queue.py

Purpose: Singleton concurrency pool. Ensures at most 3 WebCopy jobs run simultaneously. All new jobs wait in queue until a slot opens.

SignalMeaning
copy_done(slug)Pipeline site download succeeded.
copy_failed(slug, error)Pipeline download failed.
example_done(slug)Example site download succeeded.
example_failed(slug, error)Example download failed.
queue_changed()Queue length changed — update any UI counters.

Failed jobs are tracked in a _failed list. Call retry_all_failed() to re-queue them.

DeployWorker

File: core/deploy_worker.py

Purpose: Deploys a site to Cloudflare Pages via the deploy-demo.ps1 script.

SignalParametersMeaning
finished(bool, demo_url)Deployment complete. demo_url is the live HTTPS URL on success.
status(str)Current status.

Timeout: 3 minutes. The PowerShell process runs hidden (no visible window).

ColdCallWorker

File: core/cold_call_worker.py

Purpose: Runs Claude silently to extract contacts from downloaded site HTML.

Process: Runs claude -p "<prompt>" --model claude-sonnet-4-6 --effort low. Parses stdout for a JSON array. If parsing fails, falls back to regex extraction of phone numbers and emails.

SignalParametersMeaning
finished(bool, contacts_list)Scraping complete. contacts_list is a list of dicts with type and value.
status(str)Current status.

ProcessMonitor

File: core/process_monitor.py

Purpose: Watches a spawned terminal process until it exits. The AI steps (Demo, Form, Reiterate) open visible terminal windows — this worker detects when those windows close.

class ProcessMonitor(QThread):
    finished = pyqtSignal(int)  # exit code

    def run(self):
        self.process.wait()
        self.finished.emit(self.process.returncode)

The pipeline row connects to this signal to update the step status to complete (exit code 0) or failed (any other exit code).

DiscoveryWorker

File: core/discovery_worker.py

Purpose: Iterates over spiral tiles and calls discover_businesses() for each one.

SignalParametersMeaning
business_found(name, url)A new qualifying business was found. UI adds it to the queue list immediately.
log(str)Informational message for the Discovery Panel status area.
finished(int count)All tiles processed. count = total businesses found.
error(str)API error occurred.

BackupWorker

File: core/backup_worker.py

Purpose: Copies edited_sites/<slug>/ to Site Backups/<slug>/Edition N/. N is auto-incremented. Called automatically after Form/Implement completes.