""" migrate_inline_urls.py DRY-RUN by default. Yalnizca rapor verir. Eski inspection sonuclarinda result.image.url == "" kalmis kayitlari tespit eder ve image_urls JSONB sutunundaki ilk URL ile patch'ler. Bu sayede frontend history sayfasi thumbnail gosterebilir ve sonuc detay sayfasi gecerli URL ile render edilir. KULLANIM: # 1) DRY-RUN — neyi degistirecegini gor python services/backend/scripts/migrate_inline_urls.py # 2) Gercek migrasyon (yedek aldiktan sonra!) python services/backend/scripts/migrate_inline_urls.py --apply ONEMLI: Veriye dokunmadan once `pg_dump` ile yedek alin. Bu script idempotent olsa da result JSONB'leri uretim verisidir. """ from __future__ import annotations import argparse import json import logging import os import sys from typing import Any, Optional import psycopg2 from psycopg2.extras import RealDictCursor logger = logging.getLogger("migrate_inline_urls") def _patch_inline(result: Any, image_urls: Optional[list]) -> tuple[Any, bool]: """result JSONB icindeki '' URL referanslarini image_urls ile değiştir. Returns: (patched_result, changed) """ if not isinstance(result, dict): return result, False changed = False first_url = None if isinstance(image_urls, list) and image_urls: first = image_urls[0] if isinstance(first, str) and first and first != "": first_url = first img = result.get("image") if isinstance(img, dict) and img.get("url") == "": img["url"] = first_url changed = True # images: per-image listesi (yeni kontrat; eski kayitlarda yok) images_list = result.get("images") if isinstance(images_list, list): for i, entry in enumerate(images_list): if isinstance(entry, dict): url_i = image_urls[i] if (isinstance(image_urls, list) and i < len(image_urls)) else None if entry.get("url") == "": entry["url"] = url_i changed = True sub = entry.get("image") if isinstance(sub, dict) and sub.get("url") == "": sub["url"] = url_i changed = True return result, changed def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--apply", action="store_true", help="Gercek UPDATE calistir (DRY-RUN degil)") parser.add_argument( "--database-url", default=os.getenv("DATABASE_URL"), help="Postgres baglanti dizesi (default: DATABASE_URL env var)", ) args = parser.parse_args() if not args.database_url: print("HATA: DATABASE_URL belirtilmedi. --database-url veya env var saglayin.", file=sys.stderr) return 2 logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") conn = psycopg2.connect(args.database_url) conn.autocommit = False n_scanned = 0 n_dirty = 0 n_updated = 0 try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """SELECT id, image_urls, result FROM inspections WHERE result::text LIKE '%%'""" ) rows = cur.fetchall() for row in rows: n_scanned += 1 iid = row["id"] image_urls = row.get("image_urls") if isinstance(image_urls, str): try: image_urls = json.loads(image_urls) except Exception: image_urls = None result = row.get("result") if isinstance(result, str): try: result = json.loads(result) except Exception: continue patched, changed = _patch_inline(result, image_urls) if changed: n_dirty += 1 logger.info("DIRTY %s — image_urls=%s", iid, "yes" if image_urls else "EMPTY") if args.apply: with conn.cursor() as cur: cur.execute( "UPDATE inspections SET result = %s, updated_at = NOW() WHERE id = %s", (json.dumps(patched), iid), ) n_updated += 1 if args.apply: conn.commit() logger.info("COMMIT: %d kayit guncellendi", n_updated) else: conn.rollback() logger.info("DRY-RUN bitti — scanned=%d, dirty=%d (uygulamak icin --apply)", n_scanned, n_dirty) finally: conn.close() return 0 if __name__ == "__main__": sys.exit(main())