diff --git a/pipekit/web/app.py b/pipekit/web/app.py
index 77437d1..ce7d154 100644
--- a/pipekit/web/app.py
+++ b/pipekit/web/app.py
@@ -232,9 +232,51 @@ async def module_run_action(module_id: int, request: Request,
raise HTTPException(404, f"module id={module_id} not found")
run_id = repo.create_run(module_id)
background.add_task(_run_in_background, module_id, run_id, dry)
+ if request.headers.get("HX-Request"):
+ hx_target = request.headers.get("HX-Target", "")
+ if hx_target.startswith("module-status-"):
+ return _module_status_pill_response(request, module_id, force_poll=True)
+ module = repo.get_module(module_id)
+ recent_runs = repo.list_runs(module_id=module_id, limit=10,
+ exclude_status="dry_run")
+ return _templates.TemplateResponse(
+ request, "_module_live.html",
+ _ctx(module=module, recent_runs=recent_runs, force_poll=True))
return RedirectResponse(url=f"/runs/{run_id}", status_code=303)
+@_router.get("/modules/{module_id}/live-fragment", response_class=HTMLResponse)
+def module_live_fragment(request: Request, module_id: int):
+ module = repo.get_module(module_id)
+ if module is None:
+ raise HTTPException(404, f"module id={module_id} not found")
+ recent_runs = repo.list_runs(module_id=module_id, limit=10,
+ exclude_status="dry_run")
+ return _templates.TemplateResponse(
+ request, "_module_live.html",
+ _ctx(module=module, recent_runs=recent_runs, force_poll=False))
+
+
+@_router.get("/modules/{module_id}/status-pill", response_class=HTMLResponse)
+def module_status_pill(request: Request, module_id: int):
+ if repo.get_module(module_id) is None:
+ raise HTTPException(404, f"module id={module_id} not found")
+ return _module_status_pill_response(request, module_id)
+
+
+def _module_status_pill_response(request: Request, module_id: int,
+ force_poll: bool = False):
+ module = repo.get_module(module_id)
+ recent = repo.list_runs(module_id=module_id, limit=1)
+ if recent:
+ module["last_status"] = recent[0]["status"]
+ else:
+ module["last_status"] = None
+ return _templates.TemplateResponse(
+ request, "_module_status_pill.html",
+ _ctx(module=module, force_poll=force_poll))
+
+
def _run_in_background(module_id: int, run_id: int, dry_run: bool) -> None:
try:
engine.run_module(module_id, run_id=run_id, dry_run=dry_run)
diff --git a/pipekit/web/templates/_module_live.html b/pipekit/web/templates/_module_live.html
new file mode 100644
index 0000000..47bd24e
--- /dev/null
+++ b/pipekit/web/templates/_module_live.html
@@ -0,0 +1,43 @@
+{# Partial: recent runs panel + out-of-band status pill update.
+ Rendered by both module_detail.html (on load) and the live-fragment endpoint.
+ Polls every 3s while running; stops automatically when idle. #}
+
+
+
+
Recent runs
+ last {{ recent_runs|length }}
+ all →
+
+
+ {% if recent_runs %}
+
+ | id | started | duration | status | rows |
+
+ {% for r in recent_runs %}
+
+ | #{{ r.id }} |
+ {{ r.started_at or '—' }} |
+ {{ r.duration_s | duration }} |
+ {{ r.status }} |
+ {{ r.row_count if r.row_count is not none else "—" }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
No runs yet.
+ {% endif %}
+
+
+
+
+{# Out-of-band: keep the status pills in the page header in sync #}
+
+ {% if module.running %}running{% endif %}
+ {% if not module.enabled %}disabled{% endif %}
+
diff --git a/pipekit/web/templates/_module_status_pill.html b/pipekit/web/templates/_module_status_pill.html
new file mode 100644
index 0000000..9697935
--- /dev/null
+++ b/pipekit/web/templates/_module_status_pill.html
@@ -0,0 +1,18 @@
+{# Partial: status cell for one module row on the index page.
+ Swaps itself (outerHTML) every 3s while running; stops when idle. #}
+
+ {% if module.running %}
+ running
+ {% elif not module.enabled %}
+ disabled
+ {% elif module.last_status %}
+ {{ module.last_status }}
+ {% else %}
+ never ran
+ {% endif %}
+ |
diff --git a/pipekit/web/templates/module_detail.html b/pipekit/web/templates/module_detail.html
index 9f9b0a6..9d762aa 100644
--- a/pipekit/web/templates/module_detail.html
+++ b/pipekit/web/templates/module_detail.html
@@ -8,14 +8,22 @@
{{ module.name }}
module #{{ module.id }}
- {% if module.running %}running{% endif %}
- {% if not module.enabled %}disabled{% endif %}
+
+ {% if module.running %}running{% endif %}
+ {% if not module.enabled %}disabled{% endif %}
+
-
-
@@ -179,32 +187,7 @@
-
-
Recent runs
- last {{ recent_runs|length }}
- all →
-
-
- {% if recent_runs %}
-
- | id | started | duration | status | rows |
-
- {% for r in recent_runs %}
-
- | #{{ r.id }} |
- {{ r.started_at or '—' }} |
- {{ r.duration_s | duration }} |
- {{ r.status }} |
- {{ r.row_count if r.row_count is not none else "—" }} |
-
- {% endfor %}
-
-
- {% else %}
-
No runs yet.
- {% endif %}
-
-
+ {% include "_module_live.html" %}
{% endblock %}
diff --git a/pipekit/web/templates/modules_index.html b/pipekit/web/templates/modules_index.html
index 09c34e2..39b4a8b 100644
--- a/pipekit/web/templates/modules_index.html
+++ b/pipekit/web/templates/modules_index.html
@@ -34,23 +34,19 @@
{{ m.merge_strategy }} |
{{ m.dest_table }} |
{{ m.last_run_at or "—" }} |
-
- {% if m.running %}
- running
- {% elif not m.enabled %}
- disabled
- {% elif m.last_status %}
- {{ m.last_status }}
- {% else %}
- never ran
- {% endif %}
- |
+ {% with module=m %}{% include "_module_status_pill.html" %}{% endwith %}
{{ m.last_row_count if m.last_row_count is not none else "—" }} |
-
-
|