import logging from fastapi import FastAPI from sqlalchemy import text as sa_text from ..database import engine from .config import IS_DDA_MODE, ensure_library_dirs, get_storage_root, is_hf_hosted from .admin_routes import router as admin_router from .jobs_routes import router as jobs_router from .library_routes import router as library_router from .local_routes import router as local_router from .reports_routes import router as reports_router from .review_routes import router as review_router from .training_routes import router as training_router from .tree.routes import router as tree_router from .dda_auth import seed_dda_admin logger = logging.getLogger(__name__) def _migrate_detection_jobs_nullable() -> None: """SQLite legacy schema required image asset FKs; tree library jobs use paths only.""" try: with engine.connect() as conn: rows = conn.execute(sa_text("PRAGMA table_info(dda_detection_jobs)")).fetchall() if not rows: return by_name = {r[1]: r for r in rows} base_col = by_name.get("base_image_id") if not base_col or base_col[3] == 0: return logger.info("Migrating dda_detection_jobs to allow NULL image IDs for path-based jobs") conn.execute(sa_text("PRAGMA foreign_keys=OFF")) conn.execute(sa_text(""" CREATE TABLE dda_detection_jobs_new ( id INTEGER NOT NULL PRIMARY KEY, status VARCHAR(32), base_image_id INTEGER, comparison_image_id INTEGER, method VARCHAR(64), params_json TEXT, run_id INTEGER, error_message TEXT, notify_email VARCHAR(255), created_by INTEGER, started_at DATETIME, completed_at DATETIME, created_at DATETIME, FOREIGN KEY(base_image_id) REFERENCES dda_image_assets (id), FOREIGN KEY(comparison_image_id) REFERENCES dda_image_assets (id), FOREIGN KEY(run_id) REFERENCES detection_runs (id), FOREIGN KEY(created_by) REFERENCES users (id) ) """)) conn.execute(sa_text(""" INSERT INTO dda_detection_jobs_new ( id, status, base_image_id, comparison_image_id, method, params_json, run_id, error_message, notify_email, created_by, started_at, completed_at, created_at ) SELECT id, status, base_image_id, comparison_image_id, method, params_json, run_id, error_message, notify_email, created_by, started_at, completed_at, created_at FROM dda_detection_jobs """)) conn.execute(sa_text("DROP TABLE dda_detection_jobs")) conn.execute(sa_text("ALTER TABLE dda_detection_jobs_new RENAME TO dda_detection_jobs")) conn.execute(sa_text("PRAGMA foreign_keys=ON")) conn.commit() except Exception as exc: logger.warning("Detection jobs nullable migration skipped: %s", exc) def init_dda_database(): """Run DDA-specific startup tasks (dirs, seed, migrations).""" if not IS_DDA_MODE: return ensure_library_dirs() get_storage_root().mkdir(parents=True, exist_ok=True) try: with engine.connect() as conn: for stmt in ( "ALTER TABLE users ADD COLUMN role VARCHAR(32) DEFAULT 'analyst'", "ALTER TABLE detection_runs ADD COLUMN after_full_path VARCHAR(512) DEFAULT ''", ): try: conn.execute(sa_text(stmt)) conn.commit() except Exception: conn.rollback() try: conn.execute(sa_text( "CREATE UNIQUE INDEX IF NOT EXISTS ix_dda_region_reviews_run_region " "ON dda_region_reviews (run_id, region_id)" )) conn.commit() except Exception: conn.rollback() _migrate_detection_jobs_nullable() except Exception as exc: logger.warning("DDA schema migration skipped: %s", exc) from ..database import Base, SessionLocal from .tree.models import AuditLog, ImageLibrary, TreeNode # noqa: F401 try: Base.metadata.create_all(bind=engine, tables=[ TreeNode.__table__, ImageLibrary.__table__, AuditLog.__table__, ]) except Exception as exc: logger.warning("Tree table create skipped: %s", exc) db = SessionLocal() try: seed_dda_admin(db) from .tree.migration import run_tree_migration mig = run_tree_migration(db) logger.info("Tree migration: %s", mig) try: from .tree.sync_service import sync_from_filesystem sync_stats = sync_from_filesystem(db) logger.info("Filesystem sync at startup: %s", sync_stats) except Exception as exc: logger.warning("Filesystem sync at startup failed: %s", exc) from .job_runner import reconcile_stale_jobs reconcile_stale_jobs(db) finally: db.close() try: from .tree.image_service import list_all_images from ..database import SessionLocal as SL sdb = SL() try: total = len(list_all_images(sdb)) finally: sdb.close() logger.info( "DDA tree library ready (hosted=%s): %d images, storage=%s", is_hf_hosted(), total, get_storage_root(), ) except Exception as exc: logger.warning("Tree library scan at startup failed: %s", exc) def setup_dda(app: FastAPI) -> None: if not IS_DDA_MODE: logger.info("APP_MODE=legacy — DDA routes disabled") return app.include_router(tree_router, prefix="/api/dda", tags=["dda-tree"]) app.include_router(library_router, prefix="/api/dda", tags=["dda"]) app.include_router(jobs_router, prefix="/api/dda", tags=["dda-jobs"]) app.include_router(reports_router, prefix="/api/dda", tags=["dda-reports"]) app.include_router(review_router, prefix="/api/dda", tags=["dda-review"]) app.include_router(training_router, prefix="/api/dda", tags=["dda-training"]) app.include_router(admin_router, prefix="/api/dda", tags=["dda-admin"]) app.include_router(local_router, prefix="/api/dda", tags=["dda-local"]) logger.info("APP_MODE=dda — DDA routes enabled (tree, library, jobs, reports, review, training, admin, local)")