From ca335f0c69f717667738e63d3c621baabb666c17 Mon Sep 17 00:00:00 2001 From: Jens Scheidtmann Date: Tue, 28 Apr 2026 07:29:01 +0200 Subject: [PATCH 01/11] Fix Safari webtests. --- docs/source/dev_guide.rst | 8 +- python/PiFinder/server.py | 12 +-- python/PiFinder/ui/menu_manager.py | 3 + python/tests/website/test_web_equipment.py | 85 ++++++------------- python/tests/website/test_web_locations.py | 8 +- python/tests/website/test_web_network.py | 24 +++--- python/tests/website/test_web_observations.py | 4 +- python/tests/website/web_test_utils.py | 17 ++-- python/views/equipment.html | 8 +- 9 files changed, 74 insertions(+), 95 deletions(-) diff --git a/docs/source/dev_guide.rst b/docs/source/dev_guide.rst index f5d00f830..e26400035 100644 --- a/docs/source/dev_guide.rst +++ b/docs/source/dev_guide.rst @@ -349,6 +349,10 @@ the web interface works correctly. The tests exercise the remote control features of PiFinder, changing **the state of the PiFinder** and therefore should **not be run** against a PiFinder you are actively using for observing. +... tip + + Note that the whole test suite runs approximately 20 min. + Running Website Tests locally _______________________________ @@ -360,7 +364,8 @@ Note that when running the tests on Safari, you need to enable "Allow Remote Aut does not support the "headless" mode, so you will see the browser window when running the tests and you cannot use other windows while the tests are running. If you want to run the tests against a real PiFinder, set the ``PIFINDER_HOMEPAGE`` environment variable to the URL of your PiFinder instance or -pass the URL directly as a command line paramters with ``--url``. The PiFinder instance needs to be in the same WiFi as your machine, so that it is reachable via the network. +pass the URL directly as a command line parameter with ``--url``. The PiFinder instance needs to be in the same WiFi as your machine, so that it is +reachable via the network. Running Website Tests remotely ________________________________ @@ -397,7 +402,6 @@ You can also run individual tests with PyTest directly, use ``SELENIUM_GRID_URL= Note that due to the tests depending on the response times of the PiFinder web server and the Selenium Grid server, there may be occasional timeouts or failures. If you encounter such issues, simply re-run the tests. We need to strike a balance between test speed and reliability, and this may require some tuning in the future. -Note that the tests run approximately 10 minutes. Setting up Selenium Grid ___________________________ diff --git a/python/PiFinder/server.py b/python/PiFinder/server.py index b44221015..6a323589c 100644 --- a/python/PiFinder/server.py +++ b/python/PiFinder/server.py @@ -20,6 +20,7 @@ from PiFinder.multiproclogging import MultiprocLogging from flask import Flask, request, jsonify, send_file, redirect, session, make_response +from urllib.parse import quote from flask_babel import Babel, gettext # type: ignore[import-untyped] from werkzeug.routing import IntegerConverter from waitress import serve as waitress_serve @@ -52,9 +53,8 @@ def auth_wrapper(*args, **kwargs): if "authenticated" in session and session["authenticated"]: return func(*args, **kwargs) - # Store the original URL for redirect after login - session["origin_url"] = request.url - return redirect("/login") + # Pass the original URL via ?next= so Safari preserves it across redirects + return redirect(f"/login?next={quote(request.url, safe='')}") auth_wrapper.__name__ = func.__name__ return auth_wrapper @@ -253,7 +253,8 @@ def home(): def login(): if request.method == "POST": password = request.form.get("password") - origin_url = session.get("origin_url", "/") + # Read from hidden form field (set by GET handler); fall back to session + origin_url = request.form.get("origin_url") or session.get("origin_url", "/") if sys_utils.verify_password("pifinder", password): session["authenticated"] = True session.pop("origin_url", None) @@ -265,7 +266,8 @@ def login(): error_message=gettext("Invalid Password"), ) else: - origin_url = session.get("origin_url", "/") + # Prefer ?next= URL param (set by auth_required); fall back to session + origin_url = request.args.get("next", session.get("origin_url", "/")) return app.jinja_env.get_template("login.html").render( title=gettext("Login"), origin_url=origin_url ) diff --git a/python/PiFinder/ui/menu_manager.py b/python/PiFinder/ui/menu_manager.py index d838d2d69..6f93877bd 100644 --- a/python/PiFinder/ui/menu_manager.py +++ b/python/PiFinder/ui/menu_manager.py @@ -416,6 +416,9 @@ def key_long_left(self): self.help_images = None self.update() + if self.marking_menu_stack: + self.exit_marking_menu() + self.stack[-1].inactive() self.stack = self.stack[:1] self.stack[0].active() diff --git a/python/tests/website/test_web_equipment.py b/python/tests/website/test_web_equipment.py index 9a087d776..09a5db3a3 100644 --- a/python/tests/website/test_web_equipment.py +++ b/python/tests/website/test_web_equipment.py @@ -126,19 +126,14 @@ def test_equipment_instruments_table_structure(driver): _login_to_equipment(driver) # Wait for page to load - WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "h5"))) + WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "instruments-table"))) - # Find the instruments section - instruments_heading = driver.find_element( - By.XPATH, "//h5[contains(text(), 'Instruments')]" - ) + # Find the instruments section heading + instruments_heading = driver.find_element(By.ID, "instruments-heading") assert instruments_heading is not None, "Instruments heading not found" # Find the instruments table - # Look for table that comes after the instruments heading - instruments_table = driver.find_element( - By.XPATH, "//h5[contains(text(), 'Instruments')]/following-sibling::table[1]" - ) + instruments_table = driver.find_element(By.ID, "instruments-table") assert instruments_table is not None, "Instruments table not found" # Check for expected table headers @@ -177,18 +172,14 @@ def test_equipment_eyepieces_table_structure(driver): _login_to_equipment(driver) # Wait for page to load - WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "h5"))) + WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "eyepieces-table"))) - # Find the eyepieces section - eyepieces_heading = driver.find_element( - By.XPATH, "//h5[contains(text(), 'Eyepieces')]" - ) + # Find the eyepieces section heading + eyepieces_heading = driver.find_element(By.ID, "eyepieces-heading") assert eyepieces_heading is not None, "Eyepieces heading not found" # Find the eyepieces table - eyepieces_table = driver.find_element( - By.XPATH, "//h5[contains(text(), 'Eyepieces')]/following-sibling::table[1]" - ) + eyepieces_table = driver.find_element(By.ID, "eyepieces-table") assert eyepieces_table is not None, "Eyepieces table not found" # Check for expected table headers @@ -261,13 +252,9 @@ def test_equipment_add_instrument_functionality(driver): # Wait for equipment.html to load (the Instruments table only exists there, not on edit pages) instruments_table = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - ( - By.XPATH, - "//h5[contains(text(), 'Instruments')]/following-sibling::table[1]", - ) - ) + EC.presence_of_element_located((By.ID, "instruments-table")) ) + instruments_table = driver.find_element(By.ID, "instruments-table") # Look for the test instrument in the table test_instrument_found = False @@ -300,13 +287,9 @@ def test_equipment_add_instrument_functionality(driver): # then find the freshly rendered table on the new page. WebDriverWait(driver, 10).until(EC.staleness_of(old_instruments_table)) instruments_table = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - ( - By.XPATH, - "//h5[contains(text(), 'Instruments')]/following-sibling::table[1]", - ) - ) + EC.presence_of_element_located((By.ID, "instruments-table")) ) + instruments_table = driver.find_element(By.ID, "instruments-table") updated_rows = instruments_table.find_elements(By.TAG_NAME, "tr")[ 1: @@ -371,13 +354,9 @@ def test_equipment_add_eyepiece_functionality(driver): # Wait for equipment.html to load (the Eyepieces table only exists there, not on edit pages) eyepieces_table = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - ( - By.XPATH, - "//h5[contains(text(), 'Eyepieces')]/following-sibling::table[1]", - ) - ) + EC.presence_of_element_located((By.ID, "eyepieces-table")) ) + eyepieces_table = driver.find_element(By.ID, "eyepieces-table") # Look for the test eyepiece in the table test_eyepiece_found = False @@ -410,13 +389,9 @@ def test_equipment_add_eyepiece_functionality(driver): # then find the freshly rendered table on the new page. WebDriverWait(driver, 10).until(EC.staleness_of(old_eyepieces_table)) eyepieces_table = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - ( - By.XPATH, - "//h5[contains(text(), 'Eyepieces')]/following-sibling::table[1]", - ) - ) + EC.presence_of_element_located((By.ID, "eyepieces-table")) ) + eyepieces_table = driver.find_element(By.ID, "eyepieces-table") updated_rows = eyepieces_table.find_elements(By.TAG_NAME, "tr")[ 1: @@ -446,13 +421,9 @@ def test_equipment_select_active_instrument(driver): # Wait for instruments table to load instruments_table = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - ( - By.XPATH, - "//h5[contains(text(), 'Instruments')]/following-sibling::table[1]", - ) - ) + EC.presence_of_element_located((By.ID, "instruments-table")) ) + instruments_table = driver.find_element(By.ID, "instruments-table") # Get all instrument rows (skip header) instrument_rows = instruments_table.find_elements(By.TAG_NAME, "tr")[1:] @@ -493,9 +464,7 @@ def test_equipment_select_active_instrument(driver): time.sleep(1) # Verify the instrument is now active - instruments_table = driver.find_element( - By.XPATH, "//h5[contains(text(), 'Instruments')]/following-sibling::table[1]" - ) + instruments_table = driver.find_element(By.ID, "instruments-table") updated_rows = instruments_table.find_elements(By.TAG_NAME, "tr")[1:] @@ -520,13 +489,9 @@ def test_equipment_select_active_eyepiece(driver): # Wait for eyepieces table to load eyepieces_table = WebDriverWait(driver, 10).until( - EC.presence_of_element_located( - ( - By.XPATH, - "//h5[contains(text(), 'Eyepieces')]/following-sibling::table[1]", - ) - ) + EC.presence_of_element_located((By.ID, "eyepieces-table")) ) + eyepieces_table = driver.find_element(By.ID, "eyepieces-table") # Get all eyepiece rows (skip header) eyepiece_rows = eyepieces_table.find_elements(By.TAG_NAME, "tr")[1:] @@ -567,9 +532,7 @@ def test_equipment_select_active_eyepiece(driver): time.sleep(1) # Verify the eyepiece is now active - eyepieces_table = driver.find_element( - By.XPATH, "//h5[contains(text(), 'Eyepieces')]/following-sibling::table[1]" - ) + eyepieces_table = driver.find_element(By.ID, "eyepieces-table") updated_rows = eyepieces_table.find_elements(By.TAG_NAME, "tr")[1:] @@ -594,8 +557,8 @@ def _login_to_equipment(driver): # Check if we need to login (redirected to login page) try: - # Wait briefly to see if login form appears - WebDriverWait(driver, 2).until( + # Wait for login form — Safari needs more time to load after redirect + WebDriverWait(driver, 6).until( EC.presence_of_element_located((By.ID, "password")) ) # We're on the login page, use centralized login function diff --git a/python/tests/website/test_web_locations.py b/python/tests/website/test_web_locations.py index 89678db3b..5d35190b6 100644 --- a/python/tests/website/test_web_locations.py +++ b/python/tests/website/test_web_locations.py @@ -100,8 +100,8 @@ def test_locations_page_load(driver): # Check if we need to login (redirected to login page) try: - # Wait briefly to see if login form appears - WebDriverWait(driver, 2).until( + # Wait for login form — Safari needs more time to load after redirect + WebDriverWait(driver, 6).until( EC.presence_of_element_located((By.ID, "password")) ) @@ -937,8 +937,8 @@ def _login_to_interface(driver): # Check if we need to login (redirected to login page) try: - # Wait briefly to see if login form appears - WebDriverWait(driver, 2).until( + # Wait for login form — Safari needs more time to load after redirect + WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "password")) ) # We're on the login page, use centralized login function diff --git a/python/tests/website/test_web_network.py b/python/tests/website/test_web_network.py index 787fed0f3..9adb5caa1 100644 --- a/python/tests/website/test_web_network.py +++ b/python/tests/website/test_web_network.py @@ -326,17 +326,16 @@ def test_network_add_form_submission(driver): form = driver.find_element(By.ID, "new_network_form") form.submit() - # Wait for redirect back to network page and verify + # Wait for redirect back to /network (not /network/add or /network/...) WebDriverWait(driver, 10).until( - lambda driver: "/network" in driver.current_url - and "add_new=1" not in driver.current_url + lambda d: d.current_url.rstrip("/").endswith("/network") ) # Verify that the form submission was successful by checking we're back on the network page assert ( "Network Settings" in driver.page_source ), "Not on network settings page after form submission" - assert driver.current_url.endswith( + assert driver.current_url.rstrip("/").endswith( "/network" ), "URL not correct after form submission" @@ -417,11 +416,16 @@ def _login_to_network(driver): """Helper function to login and navigate to network interface""" login_to_network(driver) - # Wait for login page to load - WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "password"))) - - # Use centralized login function - login_with_password(driver) + # Detect login page by form ID, not #password — network.html also has id="password" (WiFi PSK) + try: + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, "login_form")) + ) + login_with_password(driver) + except Exception: + pass # Already authenticated; browser is on the network page # Wait for network page to load after successful login - WebDriverWait(driver, 10).until(lambda driver: "/network" in driver.current_url) + WebDriverWait(driver, 10).until( + lambda d: d.current_url.rstrip("/").endswith("/network") + ) diff --git a/python/tests/website/test_web_observations.py b/python/tests/website/test_web_observations.py index a7a7306f3..40fdcb9d2 100644 --- a/python/tests/website/test_web_observations.py +++ b/python/tests/website/test_web_observations.py @@ -62,8 +62,8 @@ def _login_to_observations(driver): # Check if we need to login (redirected to login page) try: - # Wait briefly to see if login form appears - WebDriverWait(driver, 2).until( + # Wait for login form — Safari needs more time to load after redirect + WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "password")) ) # We're on the login page, use centralized login function diff --git a/python/tests/website/web_test_utils.py b/python/tests/website/web_test_utils.py index eff9d3577..f65022acd 100644 --- a/python/tests/website/web_test_utils.py +++ b/python/tests/website/web_test_utils.py @@ -21,7 +21,10 @@ def login_to_remote(driver): """Helper function to login to remote interface""" navigate_to_page(driver, "/remote") login_with_password(driver) - # Wait for remote page to load after successful login + # Wait until the browser is actually on /remote (not still on /login after redirect) + WebDriverWait(driver, 15).until( + lambda d: "/remote" in d.current_url and "/login" not in d.current_url + ) WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "image"))) @@ -99,9 +102,9 @@ def press_keys(driver, keys): button.click() time.sleep(0.2) - # Extra delay after special button presses + # Extra delay after special button presses — Safari CSS updates need more than 1s if key_char in ["T", "Z"]: - WebDriverWait(driver, 1).until( + WebDriverWait(driver, 3).until( lambda d: "pressed" in button.get_attribute("class") ) @@ -168,16 +171,16 @@ def navigate_to_root_menu(driver) -> dict: """ Navigate to the top-level PiFinder menu, landing on the Objects item. - Sends LONG+LEFT twice (first press wakes the device and is discarded, - second resets the navigation stack to root), then presses UP six times - to reach the Start item, then DOWN twice to land on Objects. + Sends LONG+LEFT to reset the navigation stack to root (also exits any + active marking/context menu), then presses UP six times to reach the + Start item, then DOWN twice to land on Objects. Must be called while on the /remote page (i.e. after login_to_remote). Returns the /api/current-selection response dict. """ return press_keys_and_validate( driver, - "ZLZLUUUUUUDD", + "ZLUUUUUUDD", { "ui_type": "UITextMenu", "title": "PiFinder", diff --git a/python/views/equipment.html b/python/views/equipment.html index 7339d02c7..c2198e3c3 100644 --- a/python/views/equipment.html +++ b/python/views/equipment.html @@ -62,8 +62,8 @@

{{ _('Download instruments from DeepskyLog') }}

{{ _('Add new instrument') }} {{ _('Add new eyepiece') }} -
{{ _('Instruments') }}
- +
{{ _('Instruments') }}
+
@@ -110,8 +110,8 @@
{{ _('Instruments') }}
{% endfor %}
{{ _('Make') }} {{ _('Name') }}
-
{{ _('Eyepieces') }}
- +
{{ _('Eyepieces') }}
+
From 061b037ed6c0f3d26bff9301303f734cf2084e19 Mon Sep 17 00:00:00 2001 From: Jens Scheidtmann Date: Tue, 28 Apr 2026 08:31:38 +0200 Subject: [PATCH 02/11] Add GitHub action to run web tests in Chrome --- .github/workflows/web-integration-tests.yml | 119 ++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .github/workflows/web-integration-tests.yml diff --git a/.github/workflows/web-integration-tests.yml b/.github/workflows/web-integration-tests.yml new file mode 100644 index 000000000..3fd2798cd --- /dev/null +++ b/.github/workflows/web-integration-tests.yml @@ -0,0 +1,119 @@ +name: Web Integration Tests + +on: + workflow_dispatch: + +permissions: + issues: write + +jobs: + web-integration: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.9.2 + uses: actions/setup-python@v5 + with: + python-version: '3.9.2' + + - name: Create virtual environment and install dependencies + working-directory: python + run: | + python -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements_dev.txt + + - name: Set up PiFinder_data directory + run: | + mkdir -p ~/PiFinder_data/obslists + mkdir -p ~/PiFinder_data/captures + cp default_config.json ~/PiFinder_data/config.json + + - name: Start PiFinder with fake hardware + working-directory: python + run: | + source .venv/bin/activate + SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + python -m PiFinder.main -fh --camera debug --keyboard none \ + > /tmp/pifinder.log 2>&1 & + echo $! > /tmp/pifinder.pid + echo "PiFinder started with PID $(cat /tmp/pifinder.pid)" + + - name: Wait for PiFinder web server (port 8080) + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:8080/ > /dev/null 2>&1; then + echo "PiFinder web server is ready" + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: server did not start within 150s" + cat /tmp/pifinder.log + exit 1 + fi + echo "Attempt $i/30, sleeping 5s..." + sleep 5 + done + + - name: Run web tests + id: pytest_first + continue-on-error: true + working-directory: python + env: + PIFINDER_HOMEPAGE: http://localhost:8080 + run: | + source .venv/bin/activate + pytest -m web --local -v + + - name: Rerun failed tests with --lf + id: pytest_rerun + if: steps.pytest_first.outcome == 'failure' + continue-on-error: true + working-directory: python + env: + PIFINDER_HOMEPAGE: http://localhost:8080 + run: | + source .venv/bin/activate + pytest -m web --local -v --lf + + - name: Alert maintainers via GitHub Issue + if: steps.pytest_rerun.outcome == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh issue create \ + --title "Web Integration Tests failed — $(date -u '+%Y-%m-%d %H:%M UTC')" \ + --body "Web integration tests failed on both the initial run and the --lf rerun. + + **Workflow run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + **Triggered by:** ${{ github.actor }} + **Branch:** ${{ github.ref_name }}" \ + --label "bug" + + - name: Fail job after persistent test failure + if: steps.pytest_first.outcome == 'failure' && steps.pytest_rerun.outcome != 'failure' + run: | + echo "First run failed but --lf rerun passed — treating as flaky, not failing job." + + - name: Fail job if rerun also failed + if: steps.pytest_rerun.outcome == 'failure' + run: exit 1 + + - name: Upload PiFinder log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: pifinder-log + path: /tmp/pifinder.log + + - name: Stop PiFinder + if: always() + run: | + if [ -f /tmp/pifinder.pid ]; then + kill "$(cat /tmp/pifinder.pid)" || true + fi From 9555463998ec73431c8adcfea3636424f032c9b8 Mon Sep 17 00:00:00 2001 From: Jens Scheidtmann Date: Tue, 28 Apr 2026 08:54:25 +0200 Subject: [PATCH 03/11] Remove newly created github action --- .github/workflows/web-integration-tests.yml | 119 -------------------- 1 file changed, 119 deletions(-) delete mode 100644 .github/workflows/web-integration-tests.yml diff --git a/.github/workflows/web-integration-tests.yml b/.github/workflows/web-integration-tests.yml deleted file mode 100644 index 3fd2798cd..000000000 --- a/.github/workflows/web-integration-tests.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: Web Integration Tests - -on: - workflow_dispatch: - -permissions: - issues: write - -jobs: - web-integration: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python 3.9.2 - uses: actions/setup-python@v5 - with: - python-version: '3.9.2' - - - name: Create virtual environment and install dependencies - working-directory: python - run: | - python -m venv .venv - source .venv/bin/activate - pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements_dev.txt - - - name: Set up PiFinder_data directory - run: | - mkdir -p ~/PiFinder_data/obslists - mkdir -p ~/PiFinder_data/captures - cp default_config.json ~/PiFinder_data/config.json - - - name: Start PiFinder with fake hardware - working-directory: python - run: | - source .venv/bin/activate - SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ - python -m PiFinder.main -fh --camera debug --keyboard none \ - > /tmp/pifinder.log 2>&1 & - echo $! > /tmp/pifinder.pid - echo "PiFinder started with PID $(cat /tmp/pifinder.pid)" - - - name: Wait for PiFinder web server (port 8080) - run: | - for i in $(seq 1 30); do - if curl -sf http://localhost:8080/ > /dev/null 2>&1; then - echo "PiFinder web server is ready" - break - fi - if [ "$i" -eq 30 ]; then - echo "ERROR: server did not start within 150s" - cat /tmp/pifinder.log - exit 1 - fi - echo "Attempt $i/30, sleeping 5s..." - sleep 5 - done - - - name: Run web tests - id: pytest_first - continue-on-error: true - working-directory: python - env: - PIFINDER_HOMEPAGE: http://localhost:8080 - run: | - source .venv/bin/activate - pytest -m web --local -v - - - name: Rerun failed tests with --lf - id: pytest_rerun - if: steps.pytest_first.outcome == 'failure' - continue-on-error: true - working-directory: python - env: - PIFINDER_HOMEPAGE: http://localhost:8080 - run: | - source .venv/bin/activate - pytest -m web --local -v --lf - - - name: Alert maintainers via GitHub Issue - if: steps.pytest_rerun.outcome == 'failure' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh issue create \ - --title "Web Integration Tests failed — $(date -u '+%Y-%m-%d %H:%M UTC')" \ - --body "Web integration tests failed on both the initial run and the --lf rerun. - - **Workflow run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - **Triggered by:** ${{ github.actor }} - **Branch:** ${{ github.ref_name }}" \ - --label "bug" - - - name: Fail job after persistent test failure - if: steps.pytest_first.outcome == 'failure' && steps.pytest_rerun.outcome != 'failure' - run: | - echo "First run failed but --lf rerun passed — treating as flaky, not failing job." - - - name: Fail job if rerun also failed - if: steps.pytest_rerun.outcome == 'failure' - run: exit 1 - - - name: Upload PiFinder log on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: pifinder-log - path: /tmp/pifinder.log - - - name: Stop PiFinder - if: always() - run: | - if [ -f /tmp/pifinder.pid ]; then - kill "$(cat /tmp/pifinder.pid)" || true - fi From f22c42c2d67975f9a288a2f7c3a6c45ca29d449d Mon Sep 17 00:00:00 2001 From: Jens Scheidtmann Date: Fri, 1 May 2026 20:23:03 +0200 Subject: [PATCH 04/11] Fix minor issues: Logo -> main page. alignment of combo box on network page. --- python/views/base.html | 2 +- python/views/network.html | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/python/views/base.html b/python/views/base.html index b41df8d25..bec1bed15 100644 --- a/python/views/base.html +++ b/python/views/base.html @@ -12,7 +12,7 @@
{{ _('Make') }} {{ _('Name') }}