From c772d814de1eaa9fdaf667ea1cf5d2d8958b6941 Mon Sep 17 00:00:00 2001 From: Deeman Date: Wed, 25 Feb 2026 23:32:15 +0100 Subject: [PATCH] fix(pipeline): query shortcuts + schema preview + serving meta fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Shift+Enter shortcut to execute query (alongside Cmd/Ctrl+Enter) - Add ▶ preview button to schema sidebar tables: populates editor with SELECT * FROM serving. LIMIT 100 and auto-submits - Update hint text to show "Shift+Enter to run" - Overview tab: fall back to information_schema when _serving_meta.json is absent instead of showing error message; row counts show "—" - Dashboard stat cards: same fallback — query DuckDB for table count Co-Authored-By: Claude Sonnet 4.6 --- web/src/padelnomics/admin/pipeline_routes.py | 39 ++++++++++++++++--- .../admin/partials/pipeline_overview.html | 18 +++++---- .../admin/partials/pipeline_query.html | 29 +++++++++----- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/web/src/padelnomics/admin/pipeline_routes.py b/web/src/padelnomics/admin/pipeline_routes.py index 62260f4..b95f95d 100644 --- a/web/src/padelnomics/admin/pipeline_routes.py +++ b/web/src/padelnomics/admin/pipeline_routes.py @@ -372,11 +372,22 @@ def _is_stale(run: dict) -> bool: @role_required("admin") async def pipeline_dashboard(): """Main page: health stat cards + tab container.""" - summary = await asyncio.to_thread(_fetch_extraction_summary_sync) - serving_meta = await asyncio.to_thread(_load_serving_meta) + from .analytics import fetch_analytics # noqa: PLC0415 - total_serving_tables = len(serving_meta["tables"]) if serving_meta else 0 - last_export = serving_meta.get("exported_at_utc", "")[:19].replace("T", " ") if serving_meta else "—" + summary, serving_meta = await asyncio.gather( + asyncio.to_thread(_fetch_extraction_summary_sync), + asyncio.to_thread(_load_serving_meta), + ) + + if serving_meta: + total_serving_tables = len(serving_meta.get("tables", {})) + last_export = serving_meta.get("exported_at_utc", "")[:19].replace("T", " ") or "—" + else: + schema_rows = await fetch_analytics( + "SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = 'serving'" + ) + total_serving_tables = schema_rows[0]["n"] if schema_rows else 0 + last_export = "—" success_rate = 0 if summary["total"] > 0: @@ -422,12 +433,30 @@ async def pipeline_overview(): # Compute landing zone totals total_landing_bytes = sum(s["total_bytes"] for s in landing_stats) + # Build serving tables list: prefer _serving_meta.json (has counts + timestamp), + # fall back to information_schema when file doesn't exist yet. + if serving_meta: + serving_tables = [ + {"name": name, "row_count": meta.get("row_count")} + for name, meta in sorted(serving_meta.get("tables", {}).items()) + ] + last_export = serving_meta.get("exported_at_utc", "")[:19].replace("T", " ") or None + else: + from .analytics import fetch_analytics # noqa: PLC0415 + schema_rows = await fetch_analytics( + "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'serving' ORDER BY table_name" + ) + serving_tables = [{"name": r["table_name"], "row_count": None} for r in schema_rows] + last_export = None + return await render_template( "admin/partials/pipeline_overview.html", workflow_rows=workflow_rows, landing_stats=landing_stats, total_landing_bytes=total_landing_bytes, - serving_meta=serving_meta, + serving_tables=serving_tables, + last_export=last_export, format_bytes=_format_bytes, ) diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html index 7015fc5..6d5b0d0 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html +++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html @@ -56,10 +56,12 @@

Serving Tables

- {% if serving_meta %} + {% if last_export %}

- Last export: {{ serving_meta.exported_at_utc[:19].replace('T', ' ') }} + Last export: {{ last_export }}

+ {% endif %} + {% if serving_tables %}
@@ -68,18 +70,18 @@ - {% for tname, tmeta in serving_meta.tables.items() | sort %} + {% for t in serving_tables %} - - + + {% endfor %}
serving.{{ tname }}{{ "{:,}".format(tmeta.row_count) }}serving.{{ t.name }} + {% if t.row_count is not none %}{{ "{:,}".format(t.row_count) }}{% else %}—{% endif %} +
{% else %} -

- _serving_meta.json not found — run the pipeline to generate it. -

+

No serving tables found — run the pipeline first.

{% endif %} diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html index 0afb9dc..212b423 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html +++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html @@ -189,7 +189,7 @@ - {{ max_rows | default(1000) }} row limit · {{ timeout_seconds | default(10) }}s timeout · SELECT only + {{ max_rows | default(1000) }} row limit · {{ timeout_seconds | default(10) }}s timeout · SELECT only · Shift+Enter to run @@ -202,12 +202,18 @@
{% for col in cols %} @@ -241,13 +247,18 @@ document.getElementById('query-sql').addEventListener('keydown', function(e) { this.value = this.value.substring(0, start) + ' ' + this.value.substring(end); this.selectionStart = this.selectionEnd = start + 2; } - // Cmd/Ctrl+Enter submits - if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { + // Shift+Enter or Cmd/Ctrl+Enter submits + if ((e.shiftKey || e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); htmx.trigger(document.getElementById('query-form'), 'submit'); } }); +function previewTable(fullName) { + document.getElementById('query-sql').value = 'SELECT *\nFROM ' + fullName + '\nLIMIT 100'; + htmx.trigger(document.getElementById('query-form'), 'submit'); +} + function toggleSchema(name) { var cols = document.getElementById('schema-cols-' + name); var chevron = document.getElementById('schema-chevron-' + name);