Skip to content

Commit 0af5b14

Browse files
Merge pull request #4568 from springfall2008/fix/local-logo-images
fix(web): serve the Predbat logo locally instead of from GitHub
2 parents 3a5eb03 + 61bb72b commit 0af5b14

8 files changed

Lines changed: 118 additions & 8 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ plan_*.html
8787

8888
# Generated PNG files
8989
*.png
90+
!apps/predbat/bat_logo_light.png
91+
!apps/predbat/bat_logo_dark.png
9092

9193
# Build artifacts
9294
a.out

‎apps/predbat/tests/test_web_functions.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# pylint: disable=attribute-defined-outside-init
1010

1111
import asyncio
12+
import os
1213

1314
from web import WebInterface
1415
from web_helper import get_plan_renderer_js
@@ -346,3 +347,80 @@ def run_currency_unit_tests(my_predbat, web):
346347
my_predbat.num_cars = original_num_cars
347348

348349
return failed
350+
351+
352+
class FakeImageRequest:
353+
"""A minimal aiohttp-request stand-in exposing only ``match_info``."""
354+
355+
def __init__(self, filename=None):
356+
"""Store the route parameter a handler will read."""
357+
self.match_info = {} if filename is None else {"filename": filename}
358+
359+
360+
def run_web_logo_image_tests(my_predbat):
361+
"""Unit tests for the local logo image route (issue #4562).
362+
363+
The Predbat logo used to be fetched from raw.githubusercontent.com on every page
364+
load, so a GitHub outage or rate limit left the dashboard hanging for ~15s. The
365+
images now ship alongside the app and are served locally, with no network
366+
dependency at all.
367+
"""
368+
failed = 0
369+
print("**** Running web logo image tests ****")
370+
371+
web = make_web(my_predbat)
372+
373+
print("Test: each bundled logo file is served with the right content type")
374+
expected_content_types = {
375+
"bat_logo.svg": "image/svg+xml",
376+
"bat_logo_light.png": "image/png",
377+
"bat_logo_dark.png": "image/png",
378+
}
379+
for filename, content_type in expected_content_types.items():
380+
response = asyncio.run(web.html_logo_image(FakeImageRequest(filename)))
381+
if response.status != 200:
382+
print(f" ERROR: {filename} should serve with status 200, got {response.status}")
383+
failed += 1
384+
if response.content_type != content_type:
385+
print(f" ERROR: {filename} should serve as {content_type}, got {response.content_type}")
386+
failed += 1
387+
if not response.body:
388+
print(f" ERROR: {filename} should have a non-empty body")
389+
failed += 1
390+
391+
print("Test: an unknown or missing filename 404s rather than leaking the app directory")
392+
for request in [FakeImageRequest("predbat.py"), FakeImageRequest("../predbat.py"), FakeImageRequest()]:
393+
response = asyncio.run(web.html_logo_image(request))
394+
if response.status != 404:
395+
print(f" ERROR: request for {request.match_info} should 404, got {response.status}")
396+
failed += 1
397+
398+
print("Test: a whitelisted filename whose file is genuinely absent on disk 404s cleanly")
399+
# Regression for issue #4568: when an install has updated predbat.py before its self-updater has
400+
# fetched these newly-added logo files (an old download.py running against a new release), the
401+
# image genuinely won't exist yet. This should degrade to a missing image, not raise.
402+
filename = "bat_logo_light.png"
403+
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", filename)
404+
moved_path = file_path + ".test_moved"
405+
os.rename(file_path, moved_path)
406+
try:
407+
response = asyncio.run(web.html_logo_image(FakeImageRequest(filename)))
408+
if response.status != 404:
409+
print(f" ERROR: {filename} missing from disk should 404, got {response.status}")
410+
failed += 1
411+
finally:
412+
os.rename(moved_path, file_path)
413+
414+
print("Test: the page header no longer depends on GitHub for the logo")
415+
from web_helper import get_header_html
416+
417+
header = get_header_html("Test", False, "./dash", [], "v1.0", "")
418+
if "githubusercontent" in header:
419+
print(" ERROR: the page header should not reference githubusercontent.com any more")
420+
failed += 1
421+
if "./images/bat_logo" not in header:
422+
print(" ERROR: the page header should reference the local logo route")
423+
failed += 1
424+
425+
print("**** Web logo image tests completed ****")
426+
return failed

‎apps/predbat/unit_test.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
from tests.test_web_if import run_test_web_if
9191
from tests.test_web_chart_currency import test_rates_chart_series_names_use_currency_symbol
9292
from tests.test_metrics_dashboard_soc_refresh import test_soc_chart_center_text_reads_live_data
93-
from tests.test_web_functions import run_web_functions_tests
93+
from tests.test_web_functions import run_web_functions_tests, run_web_logo_image_tests
9494
from tests.test_web_history_table import run_web_history_table_tests
9595
from tests.test_web_charts import run_web_charts_tests
9696
from tests.test_web_chart_grouping import run_web_chart_grouping_tests
@@ -411,6 +411,7 @@ def main():
411411
("web_chart_currency", test_rates_chart_series_names_use_currency_symbol, "Rates chart series names follow currency_symbols tests", False),
412412
("metrics_dashboard_soc_refresh", test_soc_chart_center_text_reads_live_data, "Metrics dashboard SoC chart live-refresh tests", False),
413413
("web_functions", run_web_functions_tests, "Web function unit tests", False),
414+
("web_logo_image", run_web_logo_image_tests, "Local logo image route tests (issue #4562)", False),
414415
("web_annual", test_web_annual, "Annual web tab prefill tests", False),
415416
("web_annual_form", test_web_annual_form, "Annual web tab form tests", False),
416417
("web_annual_fast_mode", test_web_annual_fast_mode, "Annual web tab fast mode tests", False),

‎apps/predbat/web.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ async def start(self):
476476
app.router.add_post("/api/login", self.html_api_login)
477477
app.router.add_get("/browse", self.html_browse)
478478
app.router.add_get("/download", self.html_download_file)
479+
app.router.add_get("/images/{filename}", self.html_logo_image)
479480
app.router.add_get("/internals", self.html_internals)
480481
app.router.add_get("/api/internals", self.html_api_internals)
481482
app.router.add_get("/api/internals/download", self.html_api_internals_download)
@@ -5211,6 +5212,34 @@ async def html_download_file(self, request):
52115212
self.log(f"Error downloading file: {str(e)}")
52125213
return web.Response(text=f"Error downloading file: {str(e)}", status=500)
52135214

5215+
async def html_logo_image(self, request):
5216+
"""
5217+
Serve the bundled Predbat logo images locally.
5218+
5219+
The logos used to be loaded from raw.githubusercontent.com, which left the
5220+
dashboard hanging for ~15s whenever GitHub was unreachable or rate-limiting
5221+
(issue #4562). They now ship alongside the other app files so the page never
5222+
depends on internet access to render.
5223+
"""
5224+
content_types = {
5225+
"bat_logo.svg": "image/svg+xml",
5226+
"bat_logo_light.png": "image/png",
5227+
"bat_logo_dark.png": "image/png",
5228+
}
5229+
filename = request.match_info.get("filename")
5230+
content_type = content_types.get(filename)
5231+
if not content_type:
5232+
return web.Response(text="Not found", status=404)
5233+
5234+
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
5235+
try:
5236+
with open(file_path, "rb") as handle:
5237+
content = handle.read()
5238+
except OSError:
5239+
return web.Response(text="Not found", status=404)
5240+
5241+
return web.Response(body=content, content_type=content_type, headers={"Cache-Control": "public, max-age=604800"})
5242+
52145243
async def html_metrics_dashboard(self, request):
52155244
"""
52165245
Return the Metrics Dashboard page rendered inside the standard PredBat web shell.

‎apps/predbat/web_helper.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7349,8 +7349,8 @@ def get_header_html(title, calculating, default_page, arg_errors, THIS_VERSION,
73497349
"""
73507350

73517351
text = '<!doctype html><html><head><meta charset="utf-8"><title>{}</title>'.format(title)
7352-
text += '<link rel="icon" type="image/svg+xml" href="https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo.svg">'
7353-
text += '<link rel="icon" type="image/png" href="https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo_light.png">'
7352+
text += '<link rel="icon" type="image/svg+xml" href="./images/bat_logo.svg">'
7353+
text += '<link rel="icon" type="image/png" href="./images/bat_logo_light.png">'
73547354

73557355
text += """
73567356
<script>
@@ -7704,8 +7704,8 @@ def get_header_html(title, calculating, default_page, arg_errors, THIS_VERSION,
77047704
// Get the appropriate bat image based on dark/light mode
77057705
const isDarkMode = document.body.classList.contains('dark-mode');
77067706
const batImage = isDarkMode
7707-
? 'https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo_dark.png'
7708-
: 'https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo_light.png';
7707+
? './images/bat_logo_dark.png'
7708+
: './images/bat_logo_light.png';
77097709
77107710
bat.style.backgroundImage = `url('${batImage}')`;
77117711
@@ -8180,9 +8180,9 @@ def get_menu_html(calculating, default_page, arg_errors, THIS_VERSION, battery_s
81808180
<div class="menu-bar">
81818181
<div class="logo">
81828182
<img id="logo-image"
8183-
src="https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo_light.png"
8184-
data-light-src="https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo_light.png"
8185-
data-dark-src="https://raw.githubusercontent.com/springfall2008/batpred/refs/heads/main/docs/images/bat_logo_dark.png"
8183+
src="./images/bat_logo_light.png"
8184+
data-light-src="./images/bat_logo_light.png"
8185+
data-dark-src="./images/bat_logo_dark.png"
81868186
alt="Predbat Logo"
81878187
onclick="flyBat()"
81888188
style="cursor: pointer;"

0 commit comments

Comments
 (0)