```python
import time
from typing import Callable, Dict, Any, Optional

class JobQueue:
    """
    A job queue implementation with retry logic and exponential backoff.
    """
    MAX_RETRIES = 3

    def __init__(self):
        # Stores job payloads: {job_id: job_data}
        self._jobs: Dict[str, Dict[str, Any]] = {}
        # Stores job metadata, including the current retry count: {job_id: {"retries": 0}}
        self._job_metadata: Dict[str, Dict[str, int]] = {}

    def add_job(self, job_id: str, data: Dict[str, Any]):
        """
        Adds a new job to the queue.
        """
        if job_id in self._jobs:
            raise ValueError(f"Job ID {job_id} already exists.")
        self._jobs[job_id] = data
        self._job_metadata[job_id] = {"retries": 0}
        print(f"Queue: Job '{job_id}' added.")

    def process_job(self, job_id: str, processor: Callable[[Dict[str, Any]], Any]) -> bool:
        """
        Processes a job with retry logic and exponential backoff.

        Args:
            job_id: The ID of the job to process.
            processor: The function that executes the job logic.

        Returns:
            True if the job succeeded, False if all retries were exhausted.
        """
        if job_id not in self._jobs:
            print(f"Error: Job ID '{job_id}' not found.")
            return False

        current_retries = self._job_metadata[job_id]["retries"]

        while current_retries <= self.MAX_RETRIES:
            job_data = self._jobs[job_id]
            
            try:
                print(f"\n--- Processing Job '{job_id}' (Attempt {current_retries + 1}/{self.MAX_RETRIES + 1}) ---")
                processor(job_data)
                
                # Success
                print(f"✅ Job '{job_id}' succeeded on attempt {current_retries + 1}.")
                return True

            except Exception as e:
                print(f"❌ Job '{job_id}' failed on attempt {current_retries + 1}. Error: {e}")
                
                # Check if we have exhausted retries
                if current_retries >= self.MAX_RETRIES:
                    print(f"🛑 Job '{job_id}' failed permanently after {self.MAX_RETRIES + 1} attempts.")
                    return False

                # Prepare for retry
                current_retries += 1
                self._job_metadata[job_id]["retries"] = current_retries
                
                # Calculate exponential backoff delay: 2^(retries - 1) seconds
                # Since current_retries is 1 after the first failure (Attempt 2), the delay is 2^(1-1) = 1s
                # After the second failure (Attempt 3), the delay is 2^(2-1) = 2s
                # After the third failure (Attempt 4), the delay is 2^(3-1) = 4s
                delay = 2**(current_retries - 1)
                
                # Simulate delay
                print(f"⏳ Retrying job '{job_id}' in {delay} seconds (Backoff).")
                # time.sleep(delay) # Commented out to avoid actual blocking in test environment

        return False


# =======================================================================
# Example Usage
# =======================================================================

# --- Test Case 1: Successful Job (No failures) ---
print("=====================================================")
print("TEST CASE 1: Successful Job")
print("=====================================================")
queue1 = JobQueue()
queue1.add_job("job_success", {"url": "https://example.com/ok"})

def fetch_url_success(data):
    # Simulate successful request
    print(f"   [Processor] Successfully fetched {data['url']}")
    return True

success1 = queue1.process_job("job_success", fetch_url_success)
print(f"\nFinal Result (Success Job): {success1}")


# --- Test Case 2: Job that fails and recovers ---
print("\n\n=====================================================")
print("TEST CASE 2: Job that fails and recovers (2 failures)")
print("=====================================================")
queue2 = JobQueue()
queue2.add_job("job_recover", {"url": "https://example.com/unstable"})

failure_count_2 = 0
def fetch_url_recover(data):
    global failure_count_2
    if failure_count_2 < 2:
        failure_count_2 += 1
        raise ConnectionError("Network temporarily unavailable.")
    
    # Success on the 3rd attempt
    print(f"   [Processor] Successfully fetched {data['url']} after failures.")

success2 = queue2.process_job("job_recover", fetch_url_recover)
print(f"\nFinal Result (Recover Job): {success2}")


# --- Test Case 3: Job that fails permanently ---
print("\n\n=====================================================")
print("TEST CASE 3: Job that fails permanently (Max Retries)")
print("=====================================================")
queue3 = JobQueue()
queue3.add_job("job_fail", {"url": "https://example.com/bad"})

failure_count_3 = 0
def fetch_url_fail(data):
    global failure_count_3
    # Always raise an exception
    failure_count_3 += 1
    raise TimeoutError("Service timeout.")

success3 = queue3.process_job("job_fail", fetch_url_fail)
print(f"\nFinal Result (Failure Job): {success3}")
```