- 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>
Script now lives globally as a uv inline-dependency script.
Removes per-project scripts/ci.py and the msgspec dev dependency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copies ci.py from beanflows (same script, shared across projects).
Adds msgspec dev dependency required by the script.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merges worktree-h3-catchment-index. dim_locations now computes h3_cell_res5
(res 5, ~8.5km edge). location_profiles and dim_locations updated;
old location_opportunity_profile.sql already removed on master.
Conflict: location_opportunity_profile.sql deleted on master, kept deletion
and applied h3_cell_res4→res5 rename to location_profiles instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Res 4 + k_ring(1) gave ~50-60km effective radius, causing Oldenburg to
absorb Bremen (40km away) and destroying score differentiation.
Res 5 + k_ring(1) gives ~24km — captures adjacent Gemeinden (Delmenhorst
at 15km) without bleeding into unrelated cities at 40km+.
Co-Authored-By: Claude Sonnet 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>
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>
Combines city_market_profile and location_opportunity_profile into a
single serving model at (country_code, geoname_id) grain. Both Market
Score and Opportunity Score computed per location. City data enriched
via LEFT JOIN dim_cities on geoname_id.
Subtask 1/5: create new model (old models not yet removed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLMesh's extensions config supports dict form with 'repository' key,
which runs INSTALL h3 FROM community + LOAD h3 automatically at connect
time. No manual one-time install needed per machine.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add H3 res-4 regional catchment metrics (~15-18km radius, cell + 6
neighbours) to both the addressable market (25pts) and supply gap
(30pts) components of location_opportunity_profile.
Changes:
- config.yaml: add h3 to DuckDB extensions (requires one-time
INSTALL h3 FROM community on each machine)
- dim_locations: add h3_cell_res4 column via h3_latlng_to_cell()
- location_opportunity_profile: add hex_stats + catchment CTEs;
update score formula to use catchment_population and
catchment_padel_courts; expose catchment_population,
catchment_padel_courts, catchment_venues_per_100k as output cols
Motivation: local population underestimates functional market for
mid-size cities (e.g. Oldenburg ~170K misses surrounding Gemeinden).
H3 k_ring(1) captures the realistic driving-distance catchment
(~462km²) consistently across both score components.
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>
Convert the availability chain (stg_playtomic_availability →
fct_availability_slot → fct_daily_availability) from FULL to
INCREMENTAL_BY_TIME_RANGE so sqlmesh run processes only new daily
intervals instead of re-reading all files.
Supervisor changes:
- run_transform(): plan prod --auto-apply → run prod (evaluates
missing cron intervals, picks up new data)
- git_pull_and_sync(): add plan prod --auto-apply before re-exec
so model code changes are applied on deploy
- supervisor.sh: same plan → run change
Staging model uses a date-scoped glob (@start_ds) to read only
the current interval's files. snapshot_date cast to DATE (was
VARCHAR) as required by time_column.
Clean up redundant TRY_CAST(snapshot_date AS DATE) in
venue_pricing_benchmarks since it's already DATE from foundation.
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>
D1_D2_A_HW doesn't exist in the API; use D1_D4_MD5 (total labour cost
= compensation + taxes - subsidies).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>