From 054ecb1c91f52d9410047ee3dd950df1d7cc4f1b Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Sun, 26 Jul 2026 00:42:31 +0200 Subject: [PATCH] Fix: register plugin routes in create_app() not lifespan(); add status column to contacts migration 0039; add updated_at to user_tenants migration 0037 --- alembic/versions/0037_user_tenant_model.py | 5 ++++ alembic/versions/0039_contact_normalize.py | 5 ++++ app/main.py | 29 +++++++++++++++------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/alembic/versions/0037_user_tenant_model.py b/alembic/versions/0037_user_tenant_model.py index 7f664a2..e3b550a 100644 --- a/alembic/versions/0037_user_tenant_model.py +++ b/alembic/versions/0037_user_tenant_model.py @@ -80,6 +80,11 @@ def upgrade() -> None: if status_col_result is None: op.add_column("user_tenants", sa.Column("status", sa.String(20), nullable=False, server_default="active")) + # ── 4b. Add updated_at column to user_tenants ────────────────────────── + updated_col_result = conn.execute(sa.text(_column_exists("user_tenants", "updated_at"))).fetchone() + if updated_col_result is None: + op.add_column("user_tenants", sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True, server_default=sa.func.now())) + # ── 5. Data migration: copy tenant_id, role, role_id from users to user_tenants ─ # Only create UserTenant rows that don't already exist conn.execute(sa.text(""" diff --git a/alembic/versions/0039_contact_normalize.py b/alembic/versions/0039_contact_normalize.py index 9357d65..780599d 100644 --- a/alembic/versions/0039_contact_normalize.py +++ b/alembic/versions/0039_contact_normalize.py @@ -54,6 +54,11 @@ def _constraint_exists(table: str, constraint: str) -> str: def upgrade() -> None: conn = op.get_bind() + # ── 0. Add status column to contacts (for state machine) ── + status_col = conn.execute(sa.text(_column_exists("contacts", "status"))).fetchone() + if not status_col: + op.add_column("contacts", sa.Column("status", sa.String(20), nullable=False, server_default="lead")) + # ── 1a. Rename surfix → suffix ── result = conn.execute(sa.text(_column_exists("contacts", "surfix"))).fetchone() if result: diff --git a/app/main.py b/app/main.py index 81f739a..1041c85 100644 --- a/app/main.py +++ b/app/main.py @@ -139,12 +139,14 @@ async def lifespan(app: FastAPI): if plugin_record is None: # Create DB record for this builtin plugin — inactive by default (except core) + # Only core plugins auto-activate on first install + # Existing plugins that are marked active in DB will be activated below plugin_record = PluginModel( name=name, display_name=plugin.manifest.display_name, version=plugin.manifest.version, status="installed", - active=plugin.manifest.is_core, # Only core plugins auto-activate + active=plugin.manifest.is_core, is_core=plugin.manifest.is_core, ) db.add(plugin_record) @@ -170,16 +172,12 @@ async def lifespan(app: FastAPI): logger.info(f"Plugin {name} is inactive — skipping activation") continue - # Activate plugin and register routes + # Activate plugin (routes are already registered in create_app) try: await plugin.on_activate(db, container, event_bus) - for route_def in plugin.manifest.routes: - router_module = importlib.import_module(route_def.module) - router = getattr(router_module, route_def.router_attr) - app.include_router(router) plugin_record.status = "active" - print(f"[STARTUP] Activated plugin: {name} ({len(plugin.manifest.routes)} routes)", flush=True) - logger.info(f"Activated plugin: {name} ({len(plugin.manifest.routes)} routes)") + print(f"[STARTUP] Activated plugin: {name}", flush=True) + logger.info(f"Activated plugin: {name}") except Exception as exc: print(f"[STARTUP] Failed to activate plugin {name}: {exc}", flush=True) logger.error(f"Failed to activate plugin {name}: {exc}") @@ -335,7 +333,20 @@ def create_app() -> FastAPI: app.include_router(custom_fields.router) app.include_router(saved_filters.router) - # ── Plugin routes are registered in lifespan() after activation status is loaded ── + # ── Register plugin routes for all built-in plugins ── + # Routes are registered here (before app start); activation status + # is enforced at runtime via require_permission and plugin checks. + import importlib + from app.plugins.builtins.contracts import BUILTIN_PLUGINS + for name, plugin in BUILTIN_PLUGINS.items(): + try: + for route_def in plugin.manifest.routes: + router_module = importlib.import_module(route_def.module) + router = getattr(router_module, route_def.router_attr) + app.include_router(router) + logger.info(f"Registered plugin routes: {name} ({len(plugin.manifest.routes)} routes)") + except Exception as exc: + logger.error(f"Failed to register plugin routes for {name}: {exc}") # Do NOT register plugin routes here — lifespan() handles it for active plugins only # ── Serve frontend static files (SPA) ──────────────────────────────