diff --git a/web/src/padelnomics/admin/pipeline_routes.py b/web/src/padelnomics/admin/pipeline_routes.py
index cea4070..3ce3ac8 100644
--- a/web/src/padelnomics/admin/pipeline_routes.py
+++ b/web/src/padelnomics/admin/pipeline_routes.py
@@ -767,6 +767,12 @@ async def pipeline_trigger_extract():
# ── Lineage tab ───────────────────────────────────────────────────────────────
+# Compute downstream map once at import time (DAG is static).
+_DOWNSTREAM: dict[str, list[str]] = {n: [] for n in _DAG}
+for _name, _deps in _DAG.items():
+ for _dep in _deps:
+ _DOWNSTREAM.setdefault(_dep, []).append(_name)
+
@bp.route("/lineage")
@role_required("admin")
@@ -780,6 +786,67 @@ async def pipeline_lineage():
)
+@bp.route("/lineage/schema/")
+@role_required("admin")
+async def pipeline_lineage_schema(model: str):
+ """JSON: schema details for a lineage node.
+
+ Returns columns + types from information_schema (serving models only —
+ staging/foundation live in lakehouse.duckdb which the web app cannot open).
+ Row count is included for serving models when the table exists.
+ """
+ from quart import jsonify
+
+ from ..analytics import fetch_analytics
+
+ if model not in _DAG:
+ return jsonify({"error": "unknown model"}), 404
+
+ layer = _classify_layer(model)
+ upstream = _DAG[model]
+ downstream = _DOWNSTREAM.get(model, [])
+
+ row_count = None
+ columns: list[dict] = []
+
+ if layer == "serving":
+ col_rows = await fetch_analytics(
+ """
+ SELECT column_name, data_type, is_nullable
+ FROM information_schema.columns
+ WHERE table_schema = 'serving' AND table_name = ?
+ ORDER BY ordinal_position
+ """,
+ [model],
+ )
+ columns = [
+ {
+ "name": r["column_name"],
+ "type": r["data_type"],
+ "nullable": r["is_nullable"] == "YES",
+ }
+ for r in col_rows
+ ]
+ if columns:
+ # model is validated against _DAG keys — safe to interpolate
+ count_rows = await fetch_analytics(
+ f"SELECT count(*) AS n FROM serving.{model}"
+ )
+ if count_rows:
+ row_count = count_rows[0]["n"]
+
+ return jsonify(
+ {
+ "model": model,
+ "layer": layer,
+ "upstream": upstream,
+ "downstream": downstream,
+ "row_count": row_count,
+ "columns": columns,
+ }
+ )
+
+
# ── Catalog tab ───────────────────────────────────────────────────────────────
diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_lineage.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_lineage.html
index 825ed08..d112924 100644
--- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_lineage.html
+++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_lineage.html
@@ -5,45 +5,296 @@
{{ node_count }} models — staging → foundation → serving
+
+ hover to preview · click to inspect
+
{{ lineage_svg | safe }}
+
+
+
+
+
+