- Expand dim_countries.sql CASE to cover 22 missing countries (PL, RO,
CO, HU, ZA, KE, BR, CZ, QA, NZ, HR, LV, MT, CR, CY, PA, SV, DO,
PE, VE, EE, ID) that fell through to bare ISO codes
- Add 19 missing entries to COUNTRY_LABELS (i18n.py) + both locale files
(EN + DE dir_country_* keys) including IE which was in SQL but not i18n
- Localise map tooltips: routes.py injects country_name via
get_country_name(), JS uses c.country_name instead of c.country_name_en
- Localise dropdown: apply country_name filter to option labels
- Show avg + top score in map tooltip with separate color dots and new
map_score_avg / map_score_top i18n keys (EN: "Avg. Score" / "Top City",
DE: "Ø Score" / "Top-Stadt")
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace flat circle markers with nested dual-ring structure encoding
two scores per marker (core = primary, ring = secondary). Upgrade
from 3-tier to 5-tier colorblind-safe scale. Add pulse animation for
high-opportunity markers (>=75). Extract shared PNMarkers module from
3 duplicated implementations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-select user's country on opportunity map dropdown (CF-IPCountry),
auto-load the map on page load. Highlight user's city on country
overview maps with a blue ring (CF-IPCity best-effort match). Unify
opportunity score color scale to red/amber/green (was using blue for
low scores). Inject window.__GEO global for client-side geo access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Non-EU countries (AR, MX, AE, AU, etc.) previously got NULL for
median_income_pps and pli_construction, falling back to EU-calibrated
defaults (15K PPS, PLI=100) that produced wrong scores.
New World Bank WDI extractor fetches GNI per capita PPP and price level
ratio for 215 countries. dim_countries uses Germany as calibration anchor
to scale WB values into the Eurostat range (dynamic ratio, self-corrects
as both sources update). EU countries keep exact Eurostat values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CHANGELOG.md: document Market Score v4 and Opportunity Score v5 changes
- pipeline_routes.py: add dim_countries to location_profiles dependency list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update transform CLAUDE.md source integration map and conformed
dimensions table. Update CHANGELOG with unified model + tooltip
changes. Fix stale comments in dim_cities.sql and serving README.
Subtask 5/5: documentation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-article cities were fully gray (#9CA3AF), stripping informational value.
Now all cities show score-based colors (green/amber/red). Non-article cities
are differentiated via lower opacity, dashed border, desaturation, and
default cursor (no click handler). Tooltips show scores for all cities —
article cities get "Click to explore →", non-article cities get "Coming soon".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
The .card wrapper has overflow:hidden which clips Leaflet's
absolutely-positioned tile layers. Override to overflow:visible
on the rendered-article card. Add .catch() to map fetch calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The lakehouse.duckdb file uses catalog "lakehouse" not "local", causing
SQLMesh logical views to break. Script now auto-detects the catalog via
USE and falls back to physical tables when views fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add scripts/check_pipeline.py: read-only diagnostic for pricing pipeline
row counts, date range analysis, HAVING filter impact, join coverage
- Add description field to all 12 workflows in workflows.toml
- Parse and display descriptions on extraction status cards
- Show spinner + "Running" state with blue-tinted card border
- Display start time with "running..." text for active extractions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Self-hosted Leaflet 1.9.4 maps across 4 placements: markets hub
country bubbles, country overview city bubbles, city venue dots, and
a standalone opportunity map. New /api blueprint with 4 JSON endpoints.
New city_venue_locations SQLMesh serving model. No CDN — GDPR-safe.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
# CHANGELOG.md
Add real per-country cost data to ~30 calculator fields so pSEO articles
show country-specific CAPEX/OPEX instead of hardcoded DE defaults.
Extractor:
- eurostat.py: add 8 new datasets (nrg_pc_205, nrg_pc_203, lc_lci_lev,
5×prc_ppp_ind variants); add optional `dataset_code` field so multiple
dict entries can share one Eurostat API endpoint
Staging (4 new models):
- stg_electricity_prices — EUR/kWh by country, semi-annual
- stg_gas_prices — EUR/GJ by country, semi-annual
- stg_labour_costs — EUR/hour by country, annual (future staffed scenario)
- stg_price_levels — PLI indices (EU27=100) for 5 categories, annual
Foundation:
- dim_countries (new) — conformed country dimension; eliminates ~50-line CASE
blocks duplicated in dim_cities/dim_locations; computes ~29 calculator cost
override columns from PLI ratios and energy price ratios vs DE baseline;
NULL for DE so calculator falls through to DEFAULTS unchanged
- dim_cities — replace country_name/slug CASE blocks + country_income CTE
with JOIN dim_countries
- dim_locations — same refactor as dim_cities
Serving:
- pseo_city_costs_de — JOIN dim_countries; add 29 camelCase override columns
auto-applied by calculator (electricity, heating, rentSqm, hallCostSqm, …)
- planner_defaults — JOIN dim_countries; same 29 cost columns flow through
to /api/market-data endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 12 hall-building articles now link to /quote (leads.quote_request).
Previously: 2 had broken directory prose, 4 had unlinked planner mentions,
4 had broken [→ placeholder] links, 2 had scenario cards but no CTA link.
- Group 1 (bauen/build-guide): replace directory section with quote CTA
- Group 2 (kosten/risiken): link planner refs, append quote CTA
- Group 3 (finanzierung): append quote CTA after scenario card
- Group 4 (standort/businessplan): fix broken [→] links to /de|en/planner,
append quote CTA
CTA copy is contextual per article. Light-blue banner pattern, .btn class.
B2C gear articles unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the auto-escaped `{{ body_html }}` div (showed raw HTML tags)
with a sandboxed `<iframe srcdoc>` pattern matching the email preview.
Both the initial page load and the HTMX live-update endpoint now build
a full `preview_doc` document embedding the public CSS and wrapping
content in `<div class="article-body">` — pixel-perfect against the
live article, admin styles fully isolated.
Also:
- Delete ~65 lines of redundant `.preview-body` custom CSS
- Add "Meta ▾" toolbar toggle to collapse/expand metadata strip
- Add word count footer in the editor pane (updates on input)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lift admin_client fixture from 7 duplicate definitions into conftest.py.
Add mock_send_email fixture, replacing 60 inline patch() blocks across
test_emails.py, test_waitlist.py, and test_businessplan.py. Net -197 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates confirmAction() entirely. One code path: all confirmations
go through showConfirm() called by the htmx:confirm interceptor.
14 template files converted to hx-boost + hx-confirm pattern.
Pipeline endpoints updated to exclude HX-Boosted requests from the
HTMX partial path.
# Conflicts:
# web/src/padelnomics/admin/templates/admin/affiliate_form.html
# web/src/padelnomics/admin/templates/admin/affiliate_program_form.html
# web/src/padelnomics/admin/templates/admin/base_admin.html
# web/src/padelnomics/admin/templates/admin/partials/affiliate_program_results.html
# web/src/padelnomics/admin/templates/admin/partials/affiliate_row.html
Eliminate `confirmAction()` and the duplicate `cloneNode` hack entirely.
One code path: everything goes through `showConfirm()` called by the
`htmx:confirm` interceptor.
Dialog HTML:
- `<form method="dialog">` for native close semantics; button `value`
becomes `dialog.returnValue` — no manual event listener reassignment.
JS:
- `showConfirm(message)` — Promise-based, listens for `close` once.
- `htmx:confirm` handler calls `showConfirm()` and calls `issueRequest`
if confirmed. Replaces both the old HTMX handler and `confirmAction()`.
Templates (Padelnomics, 14 files):
- All `onclick=confirmAction(...)` and `onclick=confirm()` removed.
- Form-submit buttons: added `hx-boost="true"` to form + `hx-confirm`
on the submit button.
- Pure HTMX buttons (pipeline_transform, pipeline_overview): `hx-confirm`
replaces `onclick=if(!confirm(...))return false;`.
Pipeline routes (pipeline_trigger_extract, pipeline_trigger_transform):
- `is_htmx` now excludes `HX-Boosted: true` requests — boosted form
POSTs get the normal redirect instead of the inline partial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add global htmx:confirm handler in base_admin.html that intercepts
hx-confirm attributes and shows #confirm-dialog instead of window.confirm()
- Convert 4 pipeline HTMX buttons (Run Transform, Run Export, Run Full
Pipeline, Run extractor) from onclick+confirm() to hx-confirm
- Convert 4 affiliate form/list delete buttons from onclick+confirm()
to confirmAction() via event.preventDefault()
- Add scrollbar-width:none + ::-webkit-scrollbar{display:none} to
.pipeline-tabs to suppress spurious horizontal scrollbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add proxy_failure_limit param to make_tiered_cycler (default 3).
Individual proxies hitting the limit are marked dead and permanently
skipped. next_proxy() auto-escalates when all proxies in the active
tier are dead. Both mechanisms coexist: per-proxy dead tracking removes
broken individuals; tier-level threshold catches systemic failure.
- proxy.py: dead_proxies set + proxy_failure_counts dict in state;
next_proxy skips dead proxies with bounded loop; record_failure/
record_success accept optional proxy_url; dead_proxy_count() added
- playtomic_tenants.py: pass proxy_url to record_success/record_failure
- playtomic_availability.py: _worker returns (proxy_url, result);
serial loops in extract + extract_recheck capture proxy_url
- test_supervisor.py: 11 new tests in TestTieredCyclerDeadProxyTracking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>