From c5327c4012416d803d7e3cb078e7231004c0fd85 Mon Sep 17 00:00:00 2001 From: Deeman Date: Fri, 6 Mar 2026 09:01:54 +0100 Subject: [PATCH 1/2] fix(maps): move VENUE_ICON creation after Leaflet loads L.divIcon() was called at IIFE top level before the dynamic Leaflet script loaded, throwing ReferenceError and preventing all maps from rendering. Move icon creation into script.onload callback. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- web/src/padelnomics/static/js/article-maps.js | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5f5c1..7bc92d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Fixed -- **Admin template preview maps** — Leaflet maps rendered blank in admin preview due to `.card` `overflow: hidden` clipping tile layers. Set `overflow: visible` on the rendered-article card. Also added `.catch()` handlers to map fetch calls so failures are logged to console. +- **Admin template preview maps** — Leaflet maps rendered blank because `article-maps.js` called `L.divIcon()` at the IIFE top level before Leaflet was dynamically loaded, crashing the script. Moved `VENUE_ICON` creation into the `script.onload` callback so it runs after Leaflet is available. Previous commit's `.card` `overflow: visible` fix remains (clips tile layers otherwise). - **Pipeline diagnostic script** (`scripts/check_pipeline.py`) — handle DuckDB catalog naming quirk where `lakehouse.duckdb` uses catalog `lakehouse` instead of `local`, causing SQLMesh logical views to break. Script now auto-detects the catalog via `USE`, and falls back to querying physical tables (`sqlmesh__.__`) when views fail. - **Eurostat gas prices extractor** — `nrg_pc_203` filter missing `unit` dimension (API returns both KWH and GJ_GCV); now filters to `KWH`. - **Eurostat labour costs extractor** — `lc_lci_lev` used non-existent `currency` filter dimension; corrected to `unit: EUR`. diff --git a/web/src/padelnomics/static/js/article-maps.js b/web/src/padelnomics/static/js/article-maps.js index bf7753d..5824b27 100644 --- a/web/src/padelnomics/static/js/article-maps.js +++ b/web/src/padelnomics/static/js/article-maps.js @@ -62,14 +62,7 @@ .catch(function(err) { console.error('Country map fetch failed:', err); }); } - var VENUE_ICON = L.divIcon({ - className: '', - html: '
', - iconSize: [10, 10], - iconAnchor: [5, 5], - }); - - function initCityMap(el) { + function initCityMap(el, venueIcon) { var countrySlug = el.dataset.countrySlug; var citySlug = el.dataset.citySlug; var lat = parseFloat(el.dataset.lat); @@ -91,7 +84,7 @@ : '') : ''; var tip = '' + v.name + '' + (courtLine ? '
' + courtLine : ''); - L.marker([v.lat, v.lon], { icon: VENUE_ICON }) + L.marker([v.lat, v.lon], { icon: venueIcon }) .bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -7] }) .addTo(map); }); @@ -104,7 +97,15 @@ script.src = window.LEAFLET_JS_URL || '/static/vendor/leaflet/leaflet.min.js'; script.onload = function() { if (countryMapEl) initCountryMap(countryMapEl); - if (cityMapEl) initCityMap(cityMapEl); + if (cityMapEl) { + var venueIcon = L.divIcon({ + className: '', + html: '
', + iconSize: [10, 10], + iconAnchor: [5, 5], + }); + initCityMap(cityMapEl, venueIcon); + } }; document.head.appendChild(script); })(); From 831233cb290109549386f12fd74d0a906244f700 Mon Sep 17 00:00:00 2001 From: Deeman Date: Fri, 6 Mar 2026 09:34:22 +0100 Subject: [PATCH 2/2] fix(admin): add missing article_stats route, 500 handler, dev debug mode - Add /admin/articles/stats HTMX partial endpoint that was referenced by article_stats.html but never created (caused 500 during generation) - Add @app.errorhandler(500) to log exceptions with traceback - Switch dev_run.sh from Granian to Quart debug mode for browser tracebacks and auto-reload Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 ++ web/scripts/dev_run.sh | 2 +- web/src/padelnomics/admin/routes.py | 12 ++++++++++++ web/src/padelnomics/app.py | 7 +++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bc92d0..366c173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Admin template preview maps** — Leaflet maps rendered blank because `article-maps.js` called `L.divIcon()` at the IIFE top level before Leaflet was dynamically loaded, crashing the script. Moved `VENUE_ICON` creation into the `script.onload` callback so it runs after Leaflet is available. Previous commit's `.card` `overflow: visible` fix remains (clips tile layers otherwise). +- **Admin articles page 500** — `/admin/articles` crashed with `BuildError` when an article generation task was running because `article_stats.html` partial referenced `url_for('admin.article_stats')` but the route didn't exist. Added the missing HTMX partial endpoint. +- **Silent 500 errors in dev** — `dev_run.sh` used Granian which swallowed Quart's debug error pages, showing generic "Internal Server Error" with no traceback. Switched to `uv run python -m padelnomics.app` for proper debug mode with browser tracebacks. Added `@app.errorhandler(500)` to log exceptions even when running under Granian in production. - **Pipeline diagnostic script** (`scripts/check_pipeline.py`) — handle DuckDB catalog naming quirk where `lakehouse.duckdb` uses catalog `lakehouse` instead of `local`, causing SQLMesh logical views to break. Script now auto-detects the catalog via `USE`, and falls back to querying physical tables (`sqlmesh__.
__`) when views fail. - **Eurostat gas prices extractor** — `nrg_pc_203` filter missing `unit` dimension (API returns both KWH and GJ_GCV); now filters to `KWH`. - **Eurostat labour costs extractor** — `lc_lci_lev` used non-existent `currency` filter dimension; corrected to `unit: EUR`. diff --git a/web/scripts/dev_run.sh b/web/scripts/dev_run.sh index 5a1ec1d..ed275a8 100755 --- a/web/scripts/dev_run.sh +++ b/web/scripts/dev_run.sh @@ -165,7 +165,7 @@ echo "" echo "Press Ctrl-C to stop all processes." echo "" -run_with_label "$COLOR_APP" "app " uv run granian --interface asgi --host 127.0.0.1 --port 5000 --reload --reload-paths web/src padelnomics.app:app +run_with_label "$COLOR_APP" "app " uv run python -m padelnomics.app run_with_label "$COLOR_WORKER" "worker" uv run python -u -m padelnomics.worker run_with_label "$COLOR_CSS" "css " make css-watch diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index cb37dca..426922f 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -2465,6 +2465,18 @@ async def articles(): ) +@bp.route("/articles/stats") +@role_required("admin") +async def article_stats(): + """HTMX partial: article stats bar (polled while generating).""" + stats = await _get_article_stats() + return await render_template( + "admin/partials/article_stats.html", + stats=stats, + is_generating=await _is_generating(), + ) + + @bp.route("/articles/results") @role_required("admin") async def article_results(): diff --git a/web/src/padelnomics/app.py b/web/src/padelnomics/app.py index 98d3e43..1085bf3 100644 --- a/web/src/padelnomics/app.py +++ b/web/src/padelnomics/app.py @@ -270,6 +270,13 @@ def create_app() -> Quart: from .sitemap import sitemap_response return await sitemap_response(config.BASE_URL) + # Ensure unhandled exceptions are always logged (Granian doesn't show + # Quart's debug error page, so without this 500s are silent). + @app.errorhandler(500) + async def handle_500(error): + app.logger.exception("Unhandled 500 error: %s", error) + return "Internal Server Error", 500 + # Health check @app.route("/health") async def health():