- 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>
Push-notify search engines (Bing, Yandex, Seznam, Naver) when content
changes instead of waiting for sitemap crawls. Especially valuable for
batch article publishing and supplier directory updates.
- Add INDEXNOW_KEY config var and key verification route
- New seo/_indexnow.py: async fire-and-forget POST to IndexNow API
- notify_indexnow() wrapper in sitemap.py expands paths to all lang variants
- Integrated at all article publish/unpublish/edit and supplier create points
- Bulk operations batch all URLs into a single IndexNow request
- Skips silently when key is empty (dev environments)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addressable Market 20→15, Economic Power 15→10, Supply Deficit 40→50.
Update scaling description (LN/500K → SQRT/1M) and add existence
dampener explanation to supply deficit description.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 12 map_* keys to EN and DE locale files
- Inject window.__MAP_T from templates that load map scripts
- article-maps.js already used __MAP_T with fallbacks (no change needed)
- markets.html and opportunity_map.html inline scripts now use T.*
- Admin preview templates get EN fallback __MAP_T
- Fix mkt_legend_color: "Market Score" → "Padelnomics Score"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename /market-score → /padelnomics-score with 301 redirect
- Rewrite methodology page as single Padelnomics Score (pnscore_* keys)
- Replace all mscore_* i18n keys with pnscore_* in both EN and DE
- Business plan: query opportunity_score from location_profiles
- Footer link updated to padelnomics-score route
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase B: Remove dual-ring marker design (ring/core), replace with
single-color markers colored by opportunity_score ("Padelnomics Score").
Simplify CSS, update tooltips to show one score line on all maps.
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>
All three Cloudflare geo headers now available:
- g.user_country (CF-IPCountry) — used by geo-sorted article listing
- g.user_region (CF-RegionCode) — available for within-country sorting
- g.user_city (CF-IPCity) — available for city-level proximity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Read Cloudflare CF-IPCountry header into g.user_country (before_request)
- _filter_articles() sorts user's country first, nearby countries second,
then rest — falls back to published_at DESC when header is absent
- map_countries sorted so user's country bubble renders on top (Leaflet z-order)
- Nearby-country mapping covers DACH, Iberia, Nordics, Benelux, UK/IE, Americas
Prerequisite: Nginx must forward CF-IPCountry header to Quart (same as Umami).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add legend below map: bubble size = venue count, color = Market Score
- Unify opportunity score color to use same green/orange/red scale
(was using blue for low scores, inconsistent with market score)
- Add mkt_legend_size / mkt_legend_color i18n keys (EN + DE)
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>
- Delete opportunity() JSON endpoint from api.py (dead after this refactor)
- Add GET /opportunity-map/data route returning HTML partial with two JSON
data islands (opp_points + ref_points from serving.location_profiles)
- Create partials/opportunity_map_data.html (2-line data island partial)
- Rewrite opportunity_map.html: HTMX attrs on <select>, invisible #map-data
swap target, htmx:afterSwap listener replaces fetch()-based loadCountry()
city_venues endpoint stays public (article-maps.js calls it on public pages).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
opportunity_map.html (public page) still fetches these. Only countries.json
and city_venues.json are no longer called from any public page, so those two
keep @login_required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A. location_profiles.sql: supply gap now uses GREATEST(catchment_padel_courts,
COALESCE(city_padel_venue_count, 0)) so Playtomic venues prevent cities like
Murcia/Cordoba/Gijon from receiving a full 30-pt supply gap bonus when their
OSM catchment count is zero. Expected ~10-15 pt drop for affected ES cities.
B. pseo_country_overview.sql: add population-weighted lat/lon centroid columns
so the markets map can use accurate country positions from this table.
C/D. content/routes.py + markets.html: query pseo_country_overview in the route
and pass as map_countries to the template, replacing the fetch('/api/...') call
with inline JSON. Map scores now match pseo_country_overview (pop-weighted),
and the page loads without an extra round-trip.
E. api.py: add @login_required to all 4 endpoints. Unauthenticated callers get
a 302 redirect to login instead of data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defines REPO_ROOT = Path(__file__).parents[3] once in core.py.
Replaces Path(__file__).parent.parent...parent chains and Path("data/...")
CWD-relative references in admin/routes.py, content/__init__.py,
content/routes.py, and worker.py (4x local repo_root variables).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three cases: single delete, bulk by IDs, bulk apply_to_all.
Also extends _create_article() helper with article_type param.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 0029: article_type column (cornerstone/editorial/generated)
- Tab bar on /admin/articles with per-type counts
- Template filter only on Generated tab; delete guard uses article_type
- Type dropdown in article_new/edit form
- Fix: affiliate program and product Delete buttons had missing text/tag
- Bulk delete (both explicit-IDs and apply_to_all paths) now only unlinks
source .md files for generated articles (template_slug IS NOT NULL).
Manual cornerstone articles keep their .md source on disk.
- _sync_static_articles() now also renders markdown → HTML and writes to
BUILD_DIR/<lang>/<slug>.html after upserting the DB row, so cornerstones
are immediately servable after a sync without a separate rebuild step.
- scenario_pdf(): replace d = json.loads(scenario["calc_json"]) with
d = calc(state) so all current calc fields (moic, dscr, cashOnCash, …)
are present and the PDF route no longer 500s on stale stored JSON.
- Restored data/content/articles/ cornerstone .md files via git checkout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract _build_article_where() helper, eliminating duplicated WHERE
logic from _get_article_list() and _get_article_list_grouped()
- Add template_slug='__manual__' sentinel → filters template_slug IS NULL
(cornerstone / hand-written articles without a pSEO template)
- Add GET /articles/matching-count endpoint returning count of articles
matching current filter params (for the Gmail-style select-all banner)
- Extend POST /articles/bulk with apply_to_all=true mode: builds WHERE
from filter params instead of explicit IDs; rebuild capped at 2,000,
delete at 5,000
- Add "Manual" option to Template filter dropdown
- Add Gmail-style "select all matching" banner: appears when select-all
checkbox is checked, fetches total count, lets user switch to
apply_to_all mode with confirmation dialog
- Sync filter hidden inputs into bulk form on filter change; changing
filters resets apply-to-all state and clears selection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`sqlmesh run` only re-evaluates intervals for already-planned models —
it does not detect new, modified, or deleted models. Switching to
`plan prod --auto-apply` ensures schema changes (like the new
location_profiles model) are picked up automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update api.py (3 endpoints), public/routes.py, analytics.py docstring,
pipeline_routes.py DAG, pipeline_query.html placeholder, and
test_pipeline.py fixtures to use the new unified model.
Subtask 3/5: web app references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"Score X/100" → "Padelnomics Market Score: X/100" on country map (markets
hub), city map (country overview). Opportunity map uses "Padelnomics
Opportunity Score: X/100". Consistent branding across all three map views.
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>
Cities without published articles appear in muted gray and are not
clickable. The cities.json API endpoint now queries SQLite for
published articles and adds a has_article boolean to each city row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Custom error templates extending base.html with centered layout.
404 is context-aware: detects /markets/{country}/{city} paths and
shows city-specific message with link back to country overview.
Both pages support EN/DE translations.
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>
Put Leaflet init scripts inside admin_content block instead of relying
on the scripts block inheritance chain through base_admin → base.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /admin/templates/<slug>/preview/<key> page renders article HTML
directly but never loaded Leaflet CSS/JS, so country-map and city-map
divs appeared empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The admin article preview iframe was missing Leaflet CSS/JS and had
scripts blocked by the sandbox policy, so map shortcodes rendered as
empty divs.
- Extract inline map script to static/js/article-maps.js (shared
between article_detail.html and admin preview)
- Replace f-string preview doc with a proper Jinja template that
includes Leaflet assets
- Add allow-scripts to iframe sandbox on both initial load and HTMX
preview updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`plan --auto-apply` only detects SQL model changes and won't re-run
for new data. `run prod` evaluates missing cron intervals and picks
up newly extracted data — matching the fix already applied to the
supervisor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Grid children default to min-width:auto, letting the Chart.js canvas
push the container wider than its grid track. Adding min-width:0 and
overflow:hidden constrains charts to their column width.
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>
In prod the package is installed in a venv, so __file__.parents[4] doesn't
reach the repo root. Use CWD (repo root in both dev and prod via systemd
WorkingDirectory) with REPO_ROOT env var override.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>