diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a94d53..018d0b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- **pSEO error details collapse** — clicking "Error" on a job row expanded the details, but they collapsed after ~2s because HTMX polling replaced the ``. Jobs with errors now stop polling, keeping the `
` element stable. +- **UNIQUE constraint on article slug** — `ON CONFLICT(url_path, language)` upsert failed because a separate single-column `UNIQUE` on `slug` fired first. Migration 0030 drops the redundant `UNIQUE` from `slug` (keeps the index for lookups and the composite `UNIQUE(url_path, language)`). + ### Changed - **Opportunity Score v7 → v8** — better spread and discrimination across the full 0-100 range. Addressable market weight reduced (20→15 pts) with steeper sqrt curve (ceiling 1M, was LN/500K). Economic power reduced (15→10 pts). Supply deficit increased (40→50 pts) with market existence dampener: countries with zero padel venues get max 5 pts supply deficit (factor 0.1), scaling linearly to full credit at 50+ venues. NULL nearest-court distance now treated as 0 (assume nearby) instead of 0.5. Added `country_percentile` output column (PERCENT_RANK within country). Target: P5-P95 spread ≥40 pts (was 22), zero-venue countries avg <30. - **Single-score simplification** — consolidated two public-facing scores (Market Score + Opportunity Score) into one **Padelnomics Score** (internally: `opportunity_score`). All maps, tooltips, article templates, and the methodology page now show a single score. Dual-ring markers reverted to single-color markers. `/market-score` route renamed to `/padelnomics-score` (old URL 301-redirects). All `mscore_*` i18n keys replaced with `pnscore_*`. Business plan queries `opportunity_score` from `location_profiles` (replaces legacy `city_market_overview` view). Map tooltip strings now i18n'd via `window.__MAP_T` (12 keys, EN + DE). diff --git a/web/src/padelnomics/admin/templates/admin/pseo_job_status.html b/web/src/padelnomics/admin/templates/admin/pseo_job_status.html index e55bd2b..544b06c 100644 --- a/web/src/padelnomics/admin/templates/admin/pseo_job_status.html +++ b/web/src/padelnomics/admin/templates/admin/pseo_job_status.html @@ -5,7 +5,7 @@ {% set pct = [((job.progress_current / job.progress_total) * 100) | int, 100] | min if job.progress_total else 0 %} None: + # ── 1. Drop FTS triggers + virtual table ────────────────────────────────── + conn.execute("DROP TRIGGER IF EXISTS articles_ai") + conn.execute("DROP TRIGGER IF EXISTS articles_ad") + conn.execute("DROP TRIGGER IF EXISTS articles_au") + conn.execute("DROP TABLE IF EXISTS articles_fts") + + # ── 2. Recreate articles without UNIQUE on slug ─────────────────────────── + conn.execute(""" + CREATE TABLE articles_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url_path TEXT NOT NULL, + slug TEXT NOT NULL, + title TEXT NOT NULL, + meta_description TEXT, + country TEXT, + region TEXT, + og_image_url TEXT, + status TEXT NOT NULL DEFAULT 'draft', + published_at TEXT, + template_slug TEXT, + language TEXT NOT NULL DEFAULT 'en', + date_modified TEXT, + seo_head TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT, + group_key TEXT DEFAULT NULL, + noindex INTEGER NOT NULL DEFAULT 0, + article_type TEXT NOT NULL DEFAULT 'editorial', + UNIQUE(url_path, language) + ) + """) + conn.execute(""" + INSERT INTO articles_new + (id, url_path, slug, title, meta_description, country, region, + og_image_url, status, published_at, template_slug, language, + date_modified, seo_head, created_at, updated_at, group_key, + noindex, article_type) + SELECT id, url_path, slug, title, meta_description, country, region, + og_image_url, status, published_at, template_slug, language, + date_modified, seo_head, created_at, updated_at, group_key, + noindex, article_type + FROM articles + """) + conn.execute("DROP TABLE articles") + conn.execute("ALTER TABLE articles_new RENAME TO articles") + + # ── 3. Recreate indexes ─────────────────────────────────────────────────── + conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_url_path ON articles(url_path)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_url_lang ON articles(url_path, language)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_slug ON articles(slug)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_status ON articles(status, published_at)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_article_type ON articles(article_type)") + + # ── 4. Recreate FTS + triggers ──────────────────────────────────────────── + conn.execute(""" + CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5( + title, meta_description, country, region, + content='articles', content_rowid='id' + ) + """) + conn.execute(""" + CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN + INSERT INTO articles_fts(rowid, title, meta_description, country, region) + VALUES (new.id, new.title, new.meta_description, new.country, new.region); + END + """) + conn.execute(""" + CREATE TRIGGER IF NOT EXISTS articles_ad AFTER DELETE ON articles BEGIN + INSERT INTO articles_fts(articles_fts, rowid, title, meta_description, country, region) + VALUES ('delete', old.id, old.title, old.meta_description, old.country, old.region); + END + """) + conn.execute(""" + CREATE TRIGGER IF NOT EXISTS articles_au AFTER UPDATE ON articles BEGIN + INSERT INTO articles_fts(articles_fts, rowid, title, meta_description, country, region) + VALUES ('delete', old.id, old.title, old.meta_description, old.country, old.region); + INSERT INTO articles_fts(rowid, title, meta_description, country, region) + VALUES (new.id, new.title, new.meta_description, new.country, new.region); + END + """)