diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 47fc544..74d1fb7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,9 @@ concurrency:
 env:
   BIN_DIR: /tmp/cached_bin
   TEST_RELOAD_PATH: /tmp/test_basicswap
+  BSX_SELENIUM_DRIVER: firefox-ci
+  XMR_RPC_USER: xmr_user
+  XMR_RPC_PWD: xmr_pwd
 
 jobs:
   ci:
@@ -24,8 +27,18 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
       run: |
+        if [ $(dpkg-query -W -f='${Status}' firefox 2>/dev/null | grep -c "ok installed") -eq 0 ]; then
+          install -d -m 0755 /etc/apt/keyrings
+          wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
+          echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | sudo tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
+          echo "Package: *" | sudo tee /etc/apt/preferences.d/mozilla
+          echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
+          echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
+          sudo apt-get update
+          sudo apt-get install -y firefox
+        fi
         python -m pip install --upgrade pip
-        pip install flake8 codespell pytest
+        pip install -e .[dev]
         pip install -r requirements.txt --require-hashes
     - name: Install
       run: |
@@ -33,13 +46,16 @@ jobs:
         # Print the core versions to a file for caching
         basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
         cat core_versions.txt
-    - name: Running flake8
+    - name: Run flake8
       run: |
         flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
-    - name: Running codespell
+    - name: Run codespell
       run: |
         codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
-    - name: Running test_other
+    - name: Run black
+      run: |
+        black --check --diff --exclude="contrib" .
+    - name: Run test_other
       run: |
         pytest tests/basicswap/test_other.py
     - name: Cache coin cores
@@ -55,17 +71,33 @@ jobs:
       name: Running basicswap-prepare
       run: |
         basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
-    - name: Running test_xmr
+    - name: Run test_xmr
       run: |
         export PYTHONPATH=$(pwd)
         export PARTICL_BINDIR="$BIN_DIR/particl"
         export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
         export XMR_BINDIR="$BIN_DIR/monero"
         pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
-    - name: Running test_encrypted_xmr_reload
+    - name: Run test_encrypted_xmr_reload
       run: |
         export PYTHONPATH=$(pwd)
         export TEST_PATH=${TEST_RELOAD_PATH}
         mkdir -p ${TEST_PATH}/bin
         cp -r $BIN_DIR/* ${TEST_PATH}/bin/
         pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
+    - name: Run selenium tests
+      run: |
+        export TEST_PATH=/tmp/test_persistent
+        mkdir -p ${TEST_PATH}/bin
+        cp -r $BIN_DIR/* ${TEST_PATH}/bin/
+        export PYTHONPATH=$(pwd)
+        python tests/basicswap/extended/test_xmr_persistent.py > /tmp/log.txt 2>&1 & PID=$!
+        echo "Starting test_xmr_persistent, PID $PID"
+        until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
+        do
+          tail -n 1 /tmp/log.txt
+          sleep 2
+        done
+        echo "Running test_settings.py"
+        python tests/basicswap/selenium/test_settings.py
+        kill -9 $PID
diff --git a/basicswap/bin/run.py b/basicswap/bin/run.py
index d23c73b..ddcfbea 100755
--- a/basicswap/bin/run.py
+++ b/basicswap/bin/run.py
@@ -271,7 +271,9 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
     return extra_args
 
 
-def runClient(fp, data_dir, chain, start_only_coins):
+def runClient(
+    fp, data_dir: str, chain: str, start_only_coins: bool, log_prefix: str = "BasicSwap"
+):
     global swap_client, logger
     daemons = []
     pids = []
@@ -296,7 +298,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
     with open(settings_path) as fs:
         settings = json.load(fs)
 
-    swap_client = BasicSwap(fp, data_dir, settings, chain)
+    swap_client = BasicSwap(fp, data_dir, settings, chain, log_name=log_prefix)
     logger = swap_client.log
 
     if os.path.exists(pids_path):
@@ -434,7 +436,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
                         )
                     )
                     pid = daemons[-1].handle.pid
-                    swap_client.log.info("Started {} {}".format(filename, pid))
+                    swap_client.log.info(f"Started {filename} {pid}")
 
                 continue  # /decred
 
@@ -529,7 +531,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
 
     closed_pids = []
     for d in daemons:
-        swap_client.log.info("Interrupting {}".format(d.handle.pid))
+        swap_client.log.info(f"Interrupting {d.handle.pid}")
         try:
             d.handle.send_signal(
                 signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
@@ -561,7 +563,9 @@ def runClient(fp, data_dir, chain, start_only_coins):
 
 
 def printVersion():
-    logger.info("Basicswap version: %s", __version__)
+    logger.info(
+        f"Basicswap version: {__version__}",
+    )
 
 
 def printHelp():
@@ -569,9 +573,7 @@ def printHelp():
     print("\n--help, -h               Print help.")
     print("--version, -v            Print version.")
     print(
-        "--datadir=PATH           Path to basicswap data directory, default:{}.".format(
-            cfg.BASICSWAP_DATADIR
-        )
+        f"--datadir=PATH           Path to basicswap data directory, default:{cfg.BASICSWAP_DATADIR}."
     )
     print("--mainnet                Run in mainnet mode.")
     print("--testnet                Run in testnet mode.")
@@ -579,16 +581,18 @@ def printHelp():
     print(
         "--startonlycoin          Only start the provides coin daemon/s, use this if a chain requires extra processing."
     )
+    print("--logprefix              Specify log prefix.")
 
 
 def main():
     data_dir = None
     chain = "mainnet"
     start_only_coins = set()
+    log_prefix: str = "BasicSwap"
 
     for v in sys.argv[1:]:
         if len(v) < 2 or v[0] != "-":
-            logger.warning("Unknown argument %s", v)
+            logger.warning(f"Unknown argument {v}")
             continue
 
         s = v.split("=")
@@ -613,6 +617,9 @@ def main():
             if name == "datadir":
                 data_dir = os.path.expanduser(s[1])
                 continue
+            if name == "logprefix":
+                log_prefix = s[1]
+                continue
         if name == "startonlycoin":
             for coin in [s.lower() for s in s[1].split(",")]:
                 if is_known_coin(coin) is False:
@@ -620,7 +627,7 @@ def main():
                 start_only_coins.add(coin)
             continue
 
-        logger.warning("Unknown argument %s", v)
+        logger.warning(f"Unknown argument {v}")
 
     if os.name == "nt":
         logger.warning(
@@ -629,8 +636,8 @@ def main():
 
     if data_dir is None:
         data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
-    logger.info("Using datadir: %s", data_dir)
-    logger.info("Chain: %s", chain)
+    logger.info(f"Using datadir: {data_dir}")
+    logger.info(f"Chain: {chain}")
 
     if not os.path.exists(data_dir):
         os.makedirs(data_dir)
@@ -639,7 +646,7 @@ def main():
         logger.info(
             os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
         )
-        runClient(fp, data_dir, chain, start_only_coins)
+        runClient(fp, data_dir, chain, start_only_coins, log_prefix)
 
     print("Done.")
     return swap_client.fail_code if swap_client is not None else 0
diff --git a/basicswap/js_server.py b/basicswap/js_server.py
index 5e799fd..35f89fc 100644
--- a/basicswap/js_server.py
+++ b/basicswap/js_server.py
@@ -327,9 +327,7 @@ def formatBids(swap_client, bids, filters) -> bytes:
 
         amount_to = None
         if ci_to:
-            amount_to = ci_to.format_amount(
-                (b[4] * b[10]) // ci_from.COIN()
-            )
+            amount_to = ci_to.format_amount((b[4] * b[10]) // ci_from.COIN())
 
         bid_data = {
             "bid_id": b[2].hex(),
@@ -343,14 +341,13 @@ def formatBids(swap_client, bids, filters) -> bytes:
             "bid_rate": swap_client.ci(b[14]).format_amount(b[10]),
             "bid_state": strBidState(b[5]),
             "addr_from": b[11],
-            "addr_to": offer.addr_to if offer else None
+            "addr_to": offer.addr_to if offer else None,
         }
 
         if with_extra_info:
-            bid_data.update({
-                "tx_state_a": strTxState(b[7]),
-                "tx_state_b": strTxState(b[8])
-            })
+            bid_data.update(
+                {"tx_state_a": strTxState(b[7]), "tx_state_b": strTxState(b[8])}
+            )
         rv.append(bid_data)
     return bytes(json.dumps(rv), "UTF-8")
 
@@ -983,20 +980,17 @@ def js_readurl(self, url_split, post_string, is_json) -> bytes:
 def js_active(self, url_split, post_string, is_json) -> bytes:
     swap_client = self.server.swap_client
     swap_client.checkSystemStatus()
-    filters = {
-        "sort_by": "created_at",
-        "sort_dir": "desc"
-    }
+    filters = {"sort_by": "created_at", "sort_dir": "desc"}
     EXCLUDED_STATES = [
-        'Completed',
-        'Expired',
-        'Timed-out',
-        'Abandoned',
-        'Failed, refunded',
-        'Failed, swiped',
-        'Failed',
-        'Error',
-        'received'
+        "Completed",
+        "Expired",
+        "Timed-out",
+        "Abandoned",
+        "Failed, refunded",
+        "Failed, swiped",
+        "Failed",
+        "Error",
+        "received",
     ]
     all_bids = []
 
@@ -1018,8 +1012,8 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
                     "offer_id": bid[3].hex(),
                     "created_at": bid[0],
                     "bid_state": bid_state,
-                    "tx_state_a": tx_state_a if tx_state_a else 'None',
-                    "tx_state_b": tx_state_b if tx_state_b else 'None',
+                    "tx_state_a": tx_state_a if tx_state_a else "None",
+                    "tx_state_b": tx_state_b if tx_state_b else "None",
                     "coin_from": swap_client.ci(bid[9]).coin_name(),
                     "coin_to": swap_client.ci(offer.coin_to).coin_name(),
                     "amount_from": swap_client.ci(bid[9]).format_amount(bid[4]),
@@ -1029,9 +1023,9 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
                     "addr_from": bid[11],
                     "status": {
                         "main": bid_state,
-                        "initial_tx": tx_state_a if tx_state_a else 'None',
-                        "payment_tx": tx_state_b if tx_state_b else 'None'
-                    }
+                        "initial_tx": tx_state_a if tx_state_a else "None",
+                        "payment_tx": tx_state_b if tx_state_b else "None",
+                    },
                 }
                 all_bids.append(swap_data)
             except Exception:
diff --git a/basicswap/network.py b/basicswap/network.py
index 0208c6d..7e424a3 100644
--- a/basicswap/network.py
+++ b/basicswap/network.py
@@ -6,17 +6,17 @@
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
 """
-    Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
+Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
 
-    Handshake procedure:
-        node0 connecting to node1
-        node0 send_handshake
-        node1 process_handshake
-        node1 send_ping  - With a version field
-        node0 recv_ping
-            Both nodes are initialised
+Handshake procedure:
+    node0 connecting to node1
+    node0 send_handshake
+    node1 process_handshake
+    node1 send_ping  - With a version field
+    node0 recv_ping
+        Both nodes are initialised
 
-    XChaCha20_Poly1305 mac is 16bytes
+XChaCha20_Poly1305 mac is 16bytes
 """
 
 import time
diff --git a/basicswap/ui/page_bids.py b/basicswap/ui/page_bids.py
index d2de338..fb1dea8 100644
--- a/basicswap/ui/page_bids.py
+++ b/basicswap/ui/page_bids.py
@@ -151,7 +151,9 @@ def page_bid(self, url_split, post_string):
     )
 
 
-def page_bids(self, url_split, post_string, sent=False, available=False, received=False):
+def page_bids(
+    self, url_split, post_string, sent=False, available=False, received=False
+):
     server = self.server
     swap_client = server.swap_client
     swap_client.checkSystemStatus()
diff --git a/basicswap/ui/util.py b/basicswap/ui/util.py
index e4c36ac..96e3b13 100644
--- a/basicswap/ui/util.py
+++ b/basicswap/ui/util.py
@@ -145,7 +145,7 @@ def get_data_with_pagination(data, filters):
 
     offset = filters.get("offset", 0)
     limit = filters.get("limit", PAGE_LIMIT)
-    return data[offset:offset + limit]
+    return data[offset : offset + limit]
 
 
 def getTxIdHex(bid, tx_type, suffix):
diff --git a/pyproject.toml b/pyproject.toml
index d21c9e1..dbcc09f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,7 @@ dev = [
     "pip-tools",
     "pytest",
     "ruff",
-    "black",
+    "black==24.10.0",
     "selenium",
 ]
 
diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py
index 97312f1..808ac7b 100644
--- a/tests/basicswap/common_xmr.py
+++ b/tests/basicswap/common_xmr.py
@@ -572,7 +572,12 @@ class XmrTestBase(TestBase):
 
     def run_thread(self, client_id):
         client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
-        testargs = ["basicswap-run", "-datadir=" + client_path, "-regtest"]
+        testargs = [
+            "basicswap-run",
+            "-datadir=" + client_path,
+            "-regtest",
+            f"-logprefix=BSX{client_id}",
+        ]
         with patch.object(sys, "argv", testargs):
             runSystem.main()
 
diff --git a/tests/basicswap/extended/test_encrypted_xmr_reload.py b/tests/basicswap/extended/test_encrypted_xmr_reload.py
index 03f3715..445a4f8 100644
--- a/tests/basicswap/extended/test_encrypted_xmr_reload.py
+++ b/tests/basicswap/extended/test_encrypted_xmr_reload.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 # Copyright (c) 2020-2023 tecnovert
-# Copyright (c) 2024 The Basicswap developers
+# Copyright (c) 2024-2025 The Basicswap developers
 # Distributed under the MIT software license, see the accompanying
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
diff --git a/tests/basicswap/extended/test_wallet_init.py b/tests/basicswap/extended/test_wallet_init.py
index fa8fd53..a8432a7 100644
--- a/tests/basicswap/extended/test_wallet_init.py
+++ b/tests/basicswap/extended/test_wallet_init.py
@@ -66,7 +66,12 @@ class Test(unittest.TestCase):
 
     def run_thread(self, client_id):
         client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
-        testargs = ["basicswap-run", "-datadir=" + client_path, "-regtest"]
+        testargs = [
+            "basicswap-run",
+            "-datadir=" + client_path,
+            "-regtest",
+            f"-logprefix=BSX{client_id}",
+        ]
         with patch.object(sys, "argv", testargs):
             runSystem.main()
 
diff --git a/tests/basicswap/extended/test_wallet_restore.py b/tests/basicswap/extended/test_wallet_restore.py
index 67a688d..af507bb 100644
--- a/tests/basicswap/extended/test_wallet_restore.py
+++ b/tests/basicswap/extended/test_wallet_restore.py
@@ -114,7 +114,12 @@ class Test(TestBase):
 
     def run_thread(self, client_id):
         client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
-        testargs = ["basicswap-run", "-datadir=" + client_path, "-regtest"]
+        testargs = [
+            "basicswap-run",
+            "-datadir=" + client_path,
+            "-regtest",
+            f"-logprefix=BSX{client_id}",
+        ]
         with patch.object(sys, "argv", testargs):
             runSystem.main()
 
diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py
index ee63497..b99cb9d 100644
--- a/tests/basicswap/extended/test_xmr_persistent.py
+++ b/tests/basicswap/extended/test_xmr_persistent.py
@@ -7,7 +7,6 @@
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
 """
-export RESET_TEST=true
 export TEST_PATH=/tmp/test_persistent
 mkdir -p ${TEST_PATH}/bin
 cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
@@ -20,6 +19,10 @@ python tests/basicswap/extended/test_xmr_persistent.py
 # Copy coin releases to permanent storage for faster subsequent startups
 cp -r ${TEST_PATH}/bin/ ~/tmp/basicswap_bin/
 
+
+# Continue existing chains with
+export RESET_TEST=false
+
 """
 
 import json
@@ -62,7 +65,7 @@ from basicswap.interface.dcr.rpc import callrpc as callrpc_dcr
 import basicswap.bin.run as runSystem
 
 test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent"))
-RESET_TEST = make_boolean(os.getenv("RESET_TEST", "false"))
+RESET_TEST = make_boolean(os.getenv("RESET_TEST", "true"))
 
 PORT_OFS = int(os.getenv("PORT_OFS", 1))
 UI_PORT = 12700 + PORT_OFS
@@ -225,7 +228,12 @@ def signal_handler(self, sig, frame):
 
 def run_thread(self, client_id):
     client_path = os.path.join(test_path, "client{}".format(client_id))
-    testargs = ["basicswap-run", "-datadir=" + client_path, "-regtest"]
+    testargs = [
+        "basicswap-run",
+        "-datadir=" + client_path,
+        "-regtest",
+        f"-logprefix=BSX{client_id}",
+    ]
     with patch.object(sys, "argv", testargs):
         runSystem.main()
 
@@ -399,7 +407,7 @@ def start_processes(self):
 
     # Wait for height, or sequencelock is thrown off by genesis blocktime
     num_blocks = 3
-    logging.info("Waiting for Particl chain height %d", num_blocks)
+    logging.info(f"Waiting for Particl chain height {num_blocks}")
     for i in range(60):
         if self.delay_event.is_set():
             raise ValueError("Test stopped.")
@@ -448,7 +456,7 @@ class BaseTestWithPrepare(unittest.TestCase):
         if os.path.exists(test_path) and not RESET_TEST:
             logging.info(f"Continuing with existing directory: {test_path}")
         else:
-            logging.info("Preparing %d nodes.", NUM_NODES)
+            logging.info(f"Preparing {NUM_NODES} nodes.")
             prepare_nodes(
                 NUM_NODES,
                 TEST_COINS_LIST,
diff --git a/tests/basicswap/selenium/util.py b/tests/basicswap/selenium/util.py
index a166a25..74708ec 100644
--- a/tests/basicswap/selenium/util.py
+++ b/tests/basicswap/selenium/util.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 # Copyright (c) 2023 tecnovert
-# Copyright (c) 2024 The Basicswap developers
+# Copyright (c) 2024-2025 The Basicswap developers
 # Distributed under the MIT software license, see the accompanying
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
@@ -21,7 +21,17 @@ def get_driver():
     if BSX_SELENIUM_DRIVER == "firefox":
         from selenium.webdriver import Firefox, FirefoxOptions
 
-        driver = Firefox(options=FirefoxOptions())
+        options = FirefoxOptions()
+        driver = Firefox(options=options)
+    elif BSX_SELENIUM_DRIVER == "firefox-ci":
+        from selenium.webdriver import Firefox, FirefoxOptions
+
+        options = FirefoxOptions()
+        options.headless = True
+        options.add_argument("start-maximized")
+        options.add_argument("--headless")
+        options.add_argument("--no-sandbox")
+        driver = Firefox(options=options)
     elif BSX_SELENIUM_DRIVER == "chrome":
         from selenium.webdriver import Chrome, ChromeOptions
 
@@ -32,7 +42,6 @@ def get_driver():
         driver = Safari(options=SafariOptions())
     else:
         raise ValueError("Unknown driver " + BSX_SELENIUM_DRIVER)
-
     return driver
 
 
diff --git a/tests/basicswap/test_reload.py b/tests/basicswap/test_reload.py
index a4a7a50..2807530 100644
--- a/tests/basicswap/test_reload.py
+++ b/tests/basicswap/test_reload.py
@@ -80,7 +80,12 @@ class Test(unittest.TestCase):
 
     def run_thread(self, client_id):
         client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
-        testargs = ["basicswap-run", "-datadir=" + client_path, "-regtest"]
+        testargs = [
+            "basicswap-run",
+            "-datadir=" + client_path,
+            "-regtest",
+            f"-logprefix=BSX{client_id}",
+        ]
         with patch.object(sys, "argv", testargs):
             runSystem.main()
 
diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py
index f85f8f5..0b80682 100644
--- a/tests/basicswap/test_xmr.py
+++ b/tests/basicswap/test_xmr.py
@@ -660,7 +660,7 @@ class BaseTest(unittest.TestCase):
                     basicswap_dir,
                     settings,
                     "regtest",
-                    log_name="BasicSwap{}".format(i),
+                    log_name=f"BasicSwap{i}",
                 )
                 cls.swap_clients.append(sc)
                 sc.setDaemonPID(Coins.BTC, cls.btc_daemons[i].handle.pid)