#!/usr/bin/env python3 """ Migration: Add device_model column to monitoring_sessions table. Records which physical SLM model produced each session's data (e.g. "NL-43", "NL-53", "NL-32"). Used by report generation to apply the correct parsing logic without re-opening files to detect format. Run once inside the Docker container: docker exec terra-view python3 backend/migrate_add_session_device_model.py Backfill strategy for existing rows: 1. If session.unit_id is set, use roster.slm_model for that unit. 2. Else, peek at the first .rnd file in the session: presence of the 'LAeq' column header identifies AU2 / NL-32 format. Sessions where neither hint is available remain NULL — the file-content fallback in report code handles them transparently. """ import csv import io from pathlib import Path DB_PATH = Path("data/seismo_fleet.db") def _peek_first_row(abs_path: Path) -> dict: """Read only the header + first data row of an RND file. Very cheap.""" try: with open(abs_path, "r", encoding="utf-8", errors="replace") as f: reader = csv.DictReader(f) return next(reader, None) or {} except Exception: return {} def _detect_model_from_rnd(abs_path: Path) -> str | None: """Return 'NL-32' if file uses AU2 column format, else None.""" row = _peek_first_row(abs_path) if "LAeq" in row: return "NL-32" return None def migrate(): import sqlite3 if not DB_PATH.exists(): print(f"Database not found at {DB_PATH}. Are you running from /home/serversdown/terra-view?") return conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cur = conn.cursor() # ── 1. Add column (idempotent) ─────────────────────────────────────────── cur.execute("PRAGMA table_info(monitoring_sessions)") existing_cols = {row["name"] for row in cur.fetchall()} if "device_model" not in existing_cols: cur.execute("ALTER TABLE monitoring_sessions ADD COLUMN device_model TEXT") conn.commit() print("✓ Added column device_model to monitoring_sessions") else: print("○ Column device_model already exists — skipping ALTER TABLE") # ── 2. Backfill existing NULL rows ─────────────────────────────────────── cur.execute( "SELECT id, unit_id FROM monitoring_sessions WHERE device_model IS NULL" ) sessions = cur.fetchall() print(f"Backfilling {len(sessions)} session(s) with device_model=NULL...") updated = skipped = 0 for row in sessions: session_id = row["id"] unit_id = row["unit_id"] device_model = None # Strategy A: look up unit's slm_model from the roster if unit_id: cur.execute( "SELECT slm_model FROM roster WHERE id = ?", (unit_id,) ) unit_row = cur.fetchone() if unit_row and unit_row["slm_model"]: device_model = unit_row["slm_model"] # Strategy B: detect from first .rnd file in the session if device_model is None: cur.execute( """SELECT file_path FROM data_files WHERE session_id = ? AND lower(file_path) LIKE '%.rnd' LIMIT 1""", (session_id,), ) file_row = cur.fetchone() if file_row: abs_path = Path("data") / file_row["file_path"] device_model = _detect_model_from_rnd(abs_path) # None here means NL-43/NL-53 format (or unreadable file) — # leave as NULL so the existing fallback applies. if device_model: cur.execute( "UPDATE monitoring_sessions SET device_model = ? WHERE id = ?", (device_model, session_id), ) updated += 1 else: skipped += 1 conn.commit() conn.close() print(f"✓ Backfilled {updated} session(s) with a device_model.") if skipped: print( f" {skipped} session(s) left as NULL " "(no unit link and no AU2 file hint — NL-43/NL-53 or unknown; " "file-content detection applies at report time)." ) print("Migration complete.") if __name__ == "__main__": migrate()