From 207fa18fdac22ae8495dd55b2157f18fee7c7871 Mon Sep 17 00:00:00 2001 From: Deeman Date: Tue, 10 Mar 2026 21:33:38 +0100 Subject: [PATCH] fix(pseo): error details collapse + UNIQUE constraint on slug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Stop HTMX polling on job rows that have an error set, so the
element stays open when clicked (was being replaced every 2s by the poll cycle). 2. Migration 0030: drop redundant single-column UNIQUE on articles.slug — the real uniqueness key is (url_path, language). The slug index is kept for lookups. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 + .../templates/admin/pseo_job_status.html | 2 +- .../versions/0030_drop_slug_unique.py | 93 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 web/src/padelnomics/migrations/versions/0030_drop_slug_unique.py 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 + """)