The form was posting to the save route on every input change (which would
save the product on every keystroke). Added a dedicated POST
/admin/affiliate/preview route that renders the product_card.html partial
from form data without touching the database.
Form now keeps action pointing to the save route; an invisible hx-div
triggers preview-only POSTs via hx-include="#affiliate-form".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Topics: bester Schläger, Anfänger, defensiv, Fortgeschrittene, unter 100€,
Bälle, Schuhe, Ausrüstung-Guide, Zubehör, Geschenke. Each includes
[product:slug] and [product-group:category] markers, German headings,
placeholder prose, and <details> FAQ sections. Ready for editorial fill-in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added in both en.json and de.json. German uses generisches Maskulinum per
project standards. tformat-compatible {retailer} placeholder in at_retailer key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure CSS bar chart (div heights via inline %). Stats computed server-side in SQL.
Days filter (7d/30d/90d). Estimated revenue shown as rough indicator (~3% CR × €80).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
302 redirect (not 301) so every click is tracked. Extracts lang/article_slug
from Referer header best-effort. Rate-limited to 60/min per IP; clicks
above limit still redirect but are not logged to prevent amplification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure async functions: get_product(), get_products_by_category(), log_click(),
hash_ip() with daily-rotating GDPR salt, get_click_stats() with SQL aggregation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TigerStyle clean break — no backwards-compat shims for old file formats:
- stg_playtomic_{venues,opening_hours,resources}: glob updated from
*/*/tenants.jsonl.gz (2-level, old weekly) to */*/*/tenants.jsonl.gz
(3-level, new daily YYYY/MM/DD partition); blob tenants.json.gz CTE removed
- stg_playtomic_availability: morning_blob and recheck_blob CTEs removed;
only JSONL format (availability_*.jsonl.gz) is read going forward
Verified locally: stg_playtomic_venues evaluates to 14231 venues from
2026/02/28/tenants.jsonl.gz with 0 errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously fell back to len(tiers[0]) (e.g. 10 for Webshare) when
PROXY_CONCURRENCY was unset. Default is now MAX_PROXY_CONCURRENCY=200
so single-URL rotating proxies (DC/residential) run at full concurrency
without needing an explicit env var.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
git_pull_and_sync() was missing the sops decrypt step, so .env on the
server was never updated when secrets changed. Now decrypts after
checkout, before uv sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The overpass_tennis extractor has written JSONL-only since it was added.
The dual-format UNION ALL was backwards-compat debt that broke the
transform once no courts.json.gz files exist on the server:
IO Error: No files found that match the pattern
"data/landing/overpass_tennis/*/*/courts.json.gz"
Remove blob_elements CTE and the UNION ALL. Only read JSONL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- playtomic_tenants: partition by YYYY/MM/DD instead of ISO week;
schedule changed from weekly to daily in workflows.toml
- playtomic_availability: _load_tenant_ids now tries 3-level glob
(*/*/*/tenants.jsonl.gz) first for daily files, falls back to
2-level for old monthly/weekly data
Alphabetical sort would rank old monthly files above daily ones
('t' > '2' in ASCII), so the explicit fallback chain is required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tenants extractor now partitions by ISO week (e.g. 2026/W09) instead of
month (2026/02), so each weekly run writes a fresh file rather than
skipping for the rest of the month.
_load_tenant_ids() in playtomic_availability already globs */*/tenants.jsonl.gz
and sorts reverse — 'W09' > '02' alphabetically so weekly files take priority
over old monthly ones automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When DC/residential tiers have a single rotating endpoint, worker_count
defaulted to 1 (one URL = one worker). PROXY_CONCURRENCY lets you set
an explicit thread count (e.g. 100) for providers that handle concurrent
connections on a single URL.
Capped at MAX_PROXY_CONCURRENCY=200 to avoid overloading the endpoint.
Falls back to len(tiers[0]) when unset (existing behaviour).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each alert now includes a neutral category tag ([extract], [transform],
[export], [deploy], [supervisor]) and the first line of the error, so
notifications are actionable without revealing tech stack details on the
public free ntfy tier.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove outdated SSH-push model referencing GitLab variables. Document
the actual pull-based flow: Gitea Actions → tag → supervisor polls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update bootstrap_supervisor.sh and setup_server.sh to use
git.padelnomics.io:2222 instead of gitlab.com.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'run' requires the prod environment to already exist. 'plan --auto-apply'
initializes the environment on first run and applies pending changes on
subsequent runs — fully self-healing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without the 'prod' argument sqlmesh defaults to dev_<username>, which
doesn't exist on the server (padelnomics_service user).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without -R, a manual uv sync or git operation run as root would create
files under /opt/padelnomics owned by root, breaking uv for the service
user (Permission denied on .venv/bin/python3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
alpine/git sets ENTRYPOINT ["git"], so GitLab's shell executor was invoking
`git sh <script>` instead of `sh <script>`. Override with entrypoint: [""].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`credit_ledger cl` joined with `suppliers s` — both have `id`, so
SQLite raised OperationalError. Qualify as `cl.id` and `cl.supplier_id`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New route GET /admin/pipeline/lineage/schema/<model> — returns JSON
with columns+types (from information_schema for serving models),
row count, upstream and downstream model lists. Validates model
against _DAG to prevent arbitrary table access.
- Precomputes _DOWNSTREAM map at import time from _DAG.
- Lineage template: replaces minimal edge-highlight JS with full UX —
hover triggers schema prefetch + floating tooltip (layer badge, top 4
columns, "+N more" note); click opens 320px slide-in panel showing
row count, full schema table, upstream/downstream dep lists.
Dep items in panel are clickable to navigate between models.
Schema responses are cached client-side to avoid repeat fetches.
Staging/foundation models show "schema in lakehouse.duckdb only".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Propagates the conformed city key (city_slug) from dim_venues through the
full pricing pipeline, eliminating 3 fragile LOWER(TRIM(...)) fuzzy string
joins with deterministic key joins.
Changes (cascading, task-by-task):
- dim_venues: add city_slug computed column (REGEXP_REPLACE slug derivation)
- dim_venue_capacity: join foundation.dim_venues instead of stg_playtomic_venues;
carry city_slug alongside country_code/city
- fct_daily_availability: carry city_slug from dim_venue_capacity
- venue_pricing_benchmarks: carry city_slug from fct_daily_availability;
add to venue_stats GROUP BY and final SELECT/GROUP BY
- city_market_profile: join vpb on city_slug = city_slug (was LOWER(TRIM))
- planner_defaults: add city_slug to city_benchmarks CTE; join on city_slug
- pseo_city_pricing: join city_market_profile on city_slug (was LOWER(TRIM))
- pipeline_routes._DAG: dim_venue_capacity now depends on dim_venues, not stg_playtomic_venues
Result: dim_venues.city_slug → dim_cities.(country_code, city_slug) forms a
fully conformed geographic hierarchy with no fuzzy string comparisons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 5th tab to the admin pipeline page showing the full 3-layer
SQLMesh data lineage: 28 models, 35 edges across staging / foundation /
serving swim lanes.
- _DAG: canonical model dependency dict in pipeline_routes.py;
update when models are added/removed
- _classify_layer(): derives layer from name prefix (stg_/dim_fct_/rest)
- _render_lineage_svg(): pure Python SVG generator — 3-column swim lane
layout, bezier edges, color-coded per layer (green/blue/amber),
no external dependencies
- /lineage route: HTMX tab handler
- pipeline_lineage.html: partial with SVG embed + vanilla JS hover
effects (highlight connected edges, dim unrelated)
- pipeline.html: 5th "Lineage" tab button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Redirect pipeline_mark_stale to pipeline_dashboard (full page) instead
of pipeline_extractions (partial), fixing the broken CSS on form submit
- pipeline_trigger_extract accepts optional 'extractor' POST field;
validates against workflows.toml names to prevent injection, passes
as payload to enqueue("run_extraction")
- handle_run_extraction dispatches to per-extractor CLI entry point
(extract-overpass, extract-eurostat, etc.) when extractor is set,
falls back to umbrella 'extract' command otherwise
- pipeline_overview.html: add Run button to each workflow card header,
posting extractor name with CSRF token to pipeline_trigger_extract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 2a: NUTS-1 regional income for Germany (16 Bundesländer via admin1→NUTS-1 mapping)
Phase 2b: EU-wide NUTS-2 via GISCO spatial join + US Census ACS state income
- All EU-27+EFTA+UK locations now auto-resolve to NUTS-2 via ST_Contains
- Germany gets sub-Bundesland (38 Regierungsbezirke) differentiation
- US gets state-level income with PPS normalisation
- Income cascade: NUTS-2 → NUTS-1 → US state → country-level
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>