From 7fc26e0249e3d8db7e5df1c7b647ad816fa29027 Mon Sep 17 00:00:00 2001 From: Florian Bezannier Date: Thu, 18 Jun 2026 17:04:58 +0200 Subject: [PATCH] fix: home assistant proxy #1235 Use dash_app.get_asset_url() to ensure image paths respect the X-Ingress-Path header (requests_pathname_prefix). - Update all SVG image paths in control.py, figures.py, and views.py to use get_asset_url() instead of hardcoded 'assets/images/...' paths - Add test_svg_images_with_ingress_path() to verify image paths contain the correct prefix when X-Ingress-Path header is present Fixes issue where image paths were not prefixed in Home Assistant ingress proxy setups. --- psa_car_controller/web/figures.py | 9 ++++--- psa_car_controller/web/view/control.py | 9 ++++--- psa_car_controller/web/view/views.py | 7 ++--- tests/test_ha_ingress_integration.py | 37 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/psa_car_controller/web/figures.py b/psa_car_controller/web/figures.py index c5bfa191..598fe28b 100644 --- a/psa_car_controller/web/figures.py +++ b/psa_car_controller/web/figures.py @@ -12,6 +12,7 @@ from psa_car_controller.psacc.model.car import Car from psa_car_controller.psacc.repository.trips import Trip from psa_car_controller.psacc.repository.db import Database +from psa_car_controller.web.app import dash_app # pylint: disable=invalid-name from psa_car_controller.web.tools.utils import card_value_div, dash_date_to_datetime @@ -41,15 +42,15 @@ def get_summary_cards(): return {"Average consumption": {"text": [card_value_div(AVG_CONSUM_KW, "kWh/100km"), card_value_div(AVG_CONSUM_PRICE, f"{CURRENCY}/100km")], - "src": "assets/images/consumption.svg"}, + "src": dash_app.get_asset_url("images/consumption.svg")}, "Average emission": {"text": [card_value_div(AVG_EMISSION_KM, " g/km"), card_value_div(AVG_EMISSION_KW, "g/kWh")], - "src": "assets/images/pollution.svg"}, + "src": dash_app.get_asset_url("images/pollution.svg")}, "Average charge speed": {"text": [card_value_div(AVG_CHARGE_SPEED, " kW")], - "src": "assets/images/battery-charge-line.svg"}, + "src": dash_app.get_asset_url("images/battery-charge-line.svg")}, "Electricity consumption": {"text": [card_value_div(ELEC_CONSUM_KW, "kWh"), card_value_div(ELEC_CONSUM_PRICE, CURRENCY)], - "src": "assets/images/electricity bill.svg"} + "src": dash_app.get_asset_url("images/electricity bill.svg")} } diff --git a/psa_car_controller/web/view/control.py b/psa_car_controller/web/view/control.py index df1ef1ed..fb8a8de3 100644 --- a/psa_car_controller/web/view/control.py +++ b/psa_car_controller/web/view/control.py @@ -6,6 +6,7 @@ from psa_car_controller.psacc.application.psa_client import PSAClient from psa_car_controller.psacc.repository.db import Database +from psa_car_controller.web.app import dash_app from psa_car_controller.web.tools.Button import Button from psa_car_controller.web.tools.Switch import Switch from psa_car_controller.web.tools.utils import card_value_div, create_card @@ -39,18 +40,18 @@ def get_control_tabs(config): cards = OrderedDict({"Battery SOC": {"text": [card_value_div("battery_value", "%", value=convert_value_to_str( car.status.get_energy('Electric').level))], - "src": "assets/images/battery-charge.svg"}, + "src": dash_app.get_asset_url("images/battery-charge.svg")}, "Mileage": {"text": [card_value_div("mileage_value", "km", value=convert_value_to_str( car.status.timed_odometer.mileage))], - "src": "assets/images/mileage.svg"} + "src": dash_app.get_asset_url("images/mileage.svg")} }) soh = Database.get_last_soh_by_vin(car.vin) if soh: cards["Battery SOH"] = {"text": [card_value_div("battery_soh_value", "%", value=convert_value_to_str( soh))], - "src": "assets/images/battery-soh.svg"} + "src": dash_app.get_asset_url("images/battery-soh.svg")} cards.move_to_end("Mileage") el.append(dbc.Container(dbc.Row(children=create_card(cards)), fluid=True)) if config.remote_control: @@ -60,7 +61,7 @@ def get_control_tabs(config): refresh_date = car.status.get_energy('Electric').updated_at.astimezone().strftime("%X %x") buttons_row.extend([Button(REFRESH_SWITCH, car.vin, - html.Div([html.Img(src="assets/images/sync.svg", width="50px"), + html.Div([html.Img(src=dash_app.get_asset_url("images/sync.svg"), width="50px"), refresh_date]), myp.remote_client.wakeup).get_html(), Switch(CHARGE_SWITCH, car.vin, "Charge", myp.remote_client.charge_now, diff --git a/psa_car_controller/web/view/views.py b/psa_car_controller/web/view/views.py index 87e8b3c6..5c32a337 100644 --- a/psa_car_controller/web/view/views.py +++ b/psa_car_controller/web/view/views.py @@ -56,11 +56,12 @@ def add_header(el): color="secondary", className="me-1 bi bi-github", external_link=True, href=github_url) - return dbc.Row([dbc.Col(dcc.Link(html.H1('My car info'), href=dash_app.config.requests_pathname_prefix, + prefix = dash_app.config.requests_pathname_prefix or "" + return dbc.Row([dbc.Col(dcc.Link(html.H1('My car info'), href=prefix, style={"TextDecoration": "none"})), dbc.Col(html.Div([dbc_version, - dcc.Link(html.Img(src="assets/images/settings.svg", width="30veh"), - href=dash_app.config.requests_pathname_prefix + "config", + dcc.Link(html.Img(src=dash_app.get_asset_url("images/settings.svg"), width="30veh"), + href=prefix + "config", className="float-end")], className="d-grid gap-2 d-md-flex justify-content-md-end",))], className='align-items-center'), el diff --git a/tests/test_ha_ingress_integration.py b/tests/test_ha_ingress_integration.py index 7640db60..9dfba281 100644 --- a/tests/test_ha_ingress_integration.py +++ b/tests/test_ha_ingress_integration.py @@ -213,6 +213,43 @@ def test_style_json_with_ingress_path(self): except requests.exceptions.RequestException as e: self.fail(f"Request failed: {e}") + def test_svg_images_with_ingress_path(self): + """ + Test that SVG images in HTML have correct path with X-Ingress-Path header. + Specifically checks for battery-soh.svg and other image paths. + """ + prefix = '/api/ingress/a93a74ea_psacc/' + headers = { + 'X-Ingress-Path': prefix, + 'Host': f'127.0.0.1:{self.server_port}' + } + + try: + response = requests.get(self.server_url, headers=headers, timeout=10) + + self.assertEqual(response.status_code, 200, + f"Expected 200, got {response.status_code}") + + html = response.text + + # Find all src attributes in img tags + img_srcs = re.findall(r']+src="([^"]+)"', html) + + # Check each image source + for img_src in img_srcs: + # Skip data URLs + if img_src.startswith('data:'): + continue + + # Check if it's an assets/image path + if 'assets/images/' in img_src: + # The path should start with the prefix + self.assertTrue(img_src.startswith(prefix), + f"Image path {img_src} does not start with prefix {prefix}") + + except requests.exceptions.RequestException as e: + self.fail(f"Request failed: {e}") + if __name__ == '__main__': import unittest