diff --git a/servicem8_inspector.py b/servicem8_inspector.py
index d82f326..f2f29a0 100644
--- a/servicem8_inspector.py
+++ b/servicem8_inspector.py
@@ -46,6 +46,7 @@ def html_page(title: str, body: str) -> HTMLResponse:
Webhook form responses
Polled form responses
Polled quote templates
+ Apply runs
Generated materials
"""
@@ -206,6 +207,7 @@ def dashboard():
Browse polled form responses
Browse parsed polled Quote Template responses
Browse poll runs
+ Browse dry-run/apply runs
Browse generated job-material state
@@ -540,6 +542,89 @@ def form_response_detail(row_id: int):
return html_page(f"Form response {row_id}", body)
+@app.get("/poll/apply-runs", response_class=HTMLResponse)
+def list_apply_runs(page: int = Query(1, ge=1)):
+ offset = (page - 1) * PAGE_SIZE
+ try:
+ with closing(get_poll_conn()) as conn:
+ rows = conn.execute(
+ """
+ select id, form_response_uuid, job_uuid, mode, started_at, finished_at,
+ desired_count, created_count, status, error
+ from quote_template_apply_runs
+ order by id desc
+ limit ? offset ?
+ """,
+ (PAGE_SIZE, offset),
+ ).fetchall()
+ except sqlite3.Error as e:
+ return html_page("Apply runs", f"Apply-run table unavailable: {escape(str(e))}
")
+
+ table_rows = []
+ for row in rows:
+ table_rows.append(
+ f"| {row['id']} | "
+ f"{escape(row['mode'] or '')} | {escape(row['status'] or '')} | "
+ f"{escape(row['form_response_uuid'])} | "
+ f"{escape(row['job_uuid'] or '')} | {escape(row['started_at'] or '')} | {escape(row['finished_at'] or '')} | "
+ f"{row['desired_count']} | {row['created_count']} | {escape((row['error'] or '')[:160])} |
"
+ )
+
+ body = f"""
+
+ | ID | Mode | Status | Form response UUID | Job UUID | Started | Finished | Desired | Created | Error |
+ {''.join(table_rows) or "| No apply runs found yet. |
"}
+
+
+ """
+ return html_page("Apply runs", body)
+
+
+@app.get("/poll/apply-runs/{run_id}", response_class=HTMLResponse)
+def apply_run_detail(run_id: int):
+ try:
+ with closing(get_poll_conn()) as conn:
+ run = conn.execute("select * from quote_template_apply_runs where id = ?", (run_id,)).fetchone()
+ rows = conn.execute(
+ "select * from quote_template_apply_run_rows where run_id = ? order by row_index asc",
+ (run_id,),
+ ).fetchall()
+ except sqlite3.Error as e:
+ return html_page("Apply run", f"Apply-run table unavailable: {escape(str(e))}
")
+ if not run:
+ raise HTTPException(status_code=404, detail="Apply run not found")
+
+ table_rows = []
+ for row in rows:
+ payload = parse_json_field(row['api_payload_json'], {}) or {}
+ table_rows.append(
+ f"| {row['row_index']} | {escape(row['action'] or '')} | {escape(row['kind'] or '')} | "
+ f"{escape(row['name'] or '')} | {escape(row['job_material_uuid'] or '')} | "
+ f"{escape(row['source_question'] or '')} | {escape(row['error'] or '')} |
"
+ f" | API payload{escape(pretty_json(payload))} |
"
+ )
+
+ body = f"""
+
+
Run ID
{run['id']}
+
Mode
{escape(run['mode'] or '')}
+
Status
{escape(run['status'] or '')}
+
Form response UUID
+
Job UUID
{escape(run['job_uuid'] or '')}
+
Started
{escape(run['started_at'] or '')}
+
Finished
{escape(run['finished_at'] or '')}
+
Desired
{run['desired_count']}
+
Created
{run['created_count']}
+
Error
{escape(run['error'] or '')}
+
+ Rows
| # | Action | Kind | Name | Created UUID | Source question | Error |
{''.join(table_rows) or "| No rows recorded. |
"}
+ """
+ return html_page(f"Apply run {run_id}", body)
+
+
@app.get("/poll/runs", response_class=HTMLResponse)
def list_poll_runs(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
@@ -749,6 +834,22 @@ def polled_quote_template_detail(form_response_uuid: str):
f"{escape(item.get('source_question', ''))} | "
)
+ dry_run_cmd = f"/opt/webhooks/apply_polled_quote_template_jobmaterials.py --uuid {row['form_response_uuid']} --pretty"
+ apply_cmd = f"/opt/webhooks/apply_polled_quote_template_jobmaterials.py --uuid {row['form_response_uuid']} --apply --pretty"
+ try:
+ with closing(get_poll_conn()) as conn:
+ recent_runs = conn.execute(
+ "select id, mode, status, started_at, finished_at, desired_count, created_count, error from quote_template_apply_runs where form_response_uuid = ? order by id desc limit 8",
+ (row['form_response_uuid'],),
+ ).fetchall()
+ except sqlite3.Error:
+ recent_runs = []
+ recent_run_rows = []
+ for run in recent_runs:
+ recent_run_rows.append(
+ f"| {run['id']} | {escape(run['mode'] or '')} | {escape(run['status'] or '')} | {escape(run['started_at'] or '')} | {escape(run['finished_at'] or '')} | {run['desired_count']} | {run['created_count']} | {escape((run['error'] or '')[:120])} |
"
+ )
+
body = f"""
Form response UUID
{escape(row['form_response_uuid'])}
@@ -763,6 +864,19 @@ def polled_quote_template_detail(form_response_uuid: str):
Error
{escape(row['process_error'] or '')}
Raw polled response
+
+
Selective apply commands
+
+
Dry-run first:
+
{escape(dry_run_cmd)}
+
Apply to ServiceM8 only after checking the dry-run:
+
{escape(apply_cmd)}
+
+
+
+
Recent dry-run/apply runs for this response
+
| ID | Mode | Status | Started | Finished | Desired | Created | Error |
{''.join(recent_run_rows) or "| No dry-run/apply runs yet. |
"}
+
Desired jobMaterial rows
| Sort | Kind | Name | Material UUID | Source question |
{''.join(material_rows) or "| No desired jobMaterial rows. |
"}
Parsed JSON
{escape(pretty_json(parsed))}
"""