diff --git a/pipekit/web/app.py b/pipekit/web/app.py
index c852b4b..c38b4b3 100644
--- a/pipekit/web/app.py
+++ b/pipekit/web/app.py
@@ -13,6 +13,7 @@ wizard, editors, and SSE-driven live run watch come next.
from __future__ import annotations
from pathlib import Path
+from urllib.parse import urlencode
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
@@ -340,6 +341,23 @@ def wizard_step3(request: Request,
default_dest_conn_id = conn.get("default_dest_connection_id")
default_dest_schema = conn.get("default_dest_schema") or ""
+ # Proactive warning — if the default dest table already exists, surface
+ # its columns now so the user can align picks before submit.
+ dest_warn: dict | None = None
+ if not fetch_error and default_dest_conn_id and default_dest_schema:
+ dest_conn_row = repo.get_connection(default_dest_conn_id)
+ if dest_conn_row is not None:
+ try:
+ existing = _existing_dest_columns(
+ dest_conn_row, default_dest_schema, default_module_name)
+ except jrunner.JrunnerError:
+ existing = None
+ if existing is not None:
+ dest_warn = {
+ "qualified": f"{default_dest_schema}.{default_module_name}",
+ "columns": sorted(existing),
+ }
+
return _templates.TemplateResponse(
request,
"wizard_step3.html",
@@ -349,7 +367,8 @@ def wizard_step3(request: Request,
table_description=table_description,
fetch_error=fetch_error, default_module_name=default_module_name,
default_dest_conn_id=default_dest_conn_id,
- default_dest_schema=default_dest_schema),
+ default_dest_schema=default_dest_schema,
+ dest_warn=dest_warn),
)
@@ -448,12 +467,23 @@ async def wizard_create(request: Request):
missing = [c["dest_name"] for c in chosen
if c["dest_name"].lower() not in existing_cols]
if missing:
- raise HTTPException(
- 400,
- f"dest table {qualified_dest} already exists but is missing "
- f"columns: {', '.join(missing)}. Drop the table, choose a "
- f"different dest_table, or align your column picks to match "
- f"the existing schema.")
+ back_qs = urlencode(
+ [("source_connection_id", source_connection_id),
+ ("table", table),
+ ("table_schema", qvals.get("schema") or qvals.get("library") or ""),
+ *qvals.items()])
+ return _templates.TemplateResponse(
+ request,
+ "wizard_error.html",
+ _ctx(
+ title="Dest table column mismatch",
+ qualified_dest=qualified_dest,
+ missing=missing,
+ existing=sorted(existing_cols),
+ back_qs=back_qs,
+ ),
+ status_code=409,
+ )
try:
jrunner.run_dest_sql(
diff --git a/pipekit/web/static/style.css b/pipekit/web/static/style.css
index eed6905..24e85b7 100644
--- a/pipekit/web/static/style.css
+++ b/pipekit/web/static/style.css
@@ -277,3 +277,4 @@ table.picker tbody tr:hover td { background: #1c2128; }
}
.flash.ok { border-color: #2f6b35; background: #16261a; color: #b6dcb8; }
.flash.err { border-color: #6b2f2f; background: #261616; color: #dcb6b6; }
+.flash.warn { border-color: #6b5a2f; background: #261f16; color: #e1c98a; }
diff --git a/pipekit/web/templates/wizard_error.html b/pipekit/web/templates/wizard_error.html
new file mode 100644
index 0000000..5ddfbca
--- /dev/null
+++ b/pipekit/web/templates/wizard_error.html
@@ -0,0 +1,57 @@
+{% extends "base.html" %}
+{% set section = "modules" %}
+{% block title %}New module — error{% endblock %}
+
+{% block content %}
+
+
+
+
+ Dest table {{ qualified_dest }} already exists, but your
+ picks don't match its schema. Drop the table, choose a different
+ dest table, or align your column picks (dest names) with the
+ existing columns below.
+
+
+
+
+
+ Missing from existing table
+ {{ missing|length }}
+
+
+
+
+ {% for m in missing %}
+ | {{ m }} |
+ {% endfor %}
+
+
+
+
+
+
+
+ Columns in existing dest
+ {{ existing|length }}
+
+
+
+
+ {% for c in existing %}
+ | {{ c }} |
+ {% endfor %}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/pipekit/web/templates/wizard_step3.html b/pipekit/web/templates/wizard_step3.html
index d8db190..04d9a7d 100644
--- a/pipekit/web/templates/wizard_step3.html
+++ b/pipekit/web/templates/wizard_step3.html
@@ -18,6 +18,16 @@
+{% if dest_warn %}
+
+ Dest table {{ dest_warn.qualified }} already exists on the
+ default destination. If you proceed, pipekit will not drop
+ or recreate it — your picks below (dest names) must match the existing
+ columns, or the create will fail. Existing columns:
+ {{ dest_warn.columns|join(', ') }}
+
+{% endif %}
+
{% if not fetch_error %}