Files
plumbing/servicem8_inspector.py
T

1083 lines
55 KiB
Python

import json
import os
import sqlite3
from contextlib import closing
from datetime import datetime
from html import escape
from urllib.parse import urlencode
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse
DB_PATH = os.getenv("WEBHOOK_DB_PATH", "./servicem8_webhooks.db")
STATE_DB_PATH = os.getenv("WEBHOOK_STATE_DB_PATH", "./servicem8_quote_materials_state.db")
POLL_DB_PATH = os.getenv("WEBHOOK_POLL_DB_PATH", "./servicem8_formresponse_poll.db")
APP_HOST = os.getenv("WEBHOOK_INSPECTOR_HOST", "0.0.0.0")
APP_PORT = int(os.getenv("WEBHOOK_INSPECTOR_PORT", "18355"))
PAGE_SIZE = 50
app = FastAPI(title="ServiceM8 Inspector", version="0.1.0")
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def get_state_conn():
conn = sqlite3.connect(STATE_DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def get_poll_conn():
conn = sqlite3.connect(POLL_DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def html_page(title: str, body: str) -> HTMLResponse:
nav = """
<nav>
<a href='/'>Dashboard</a>
<a href='/events'>Events</a>
<a href='/objects'>Objects</a>
<a href='/form-responses'>Webhook form responses</a>
<a href='/poll/form-responses'>Polled form responses</a>
<a href='/poll/quote-template'>Polled quote templates</a>
<a href='/poll/apply-runs'>Apply runs</a>
<a href='/poll/remote-existing-incidents'>Remote existing incidents</a>
<a href='/generated-materials'>Generated materials</a>
</nav>
"""
css = """
<style>
body { font-family: Inter, system-ui, sans-serif; margin: 24px; color: #1f2937; background: #f8fafc; }
nav { margin-bottom: 20px; display: flex; gap: 14px; flex-wrap: wrap; }
nav a { text-decoration: none; color: #0f766e; font-weight: 600; }
h1, h2, h3 { margin-bottom: 0.4rem; }
.muted { color: #6b7280; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin: 16px 0 24px; }
.card { background: white; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.big { font-size: 1.6rem; font-weight: 700; }
table { width: 100%; border-collapse: collapse; background: white; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
th { background: #f1f5f9; position: sticky; top: 0; }
tr:hover td { background: #f8fafc; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
pre { white-space: pre-wrap; word-break: break-word; background: #0f172a; color: #e2e8f0; padding: 14px; border-radius: 10px; overflow-x: auto; }
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #e0f2fe; color: #075985; font-size: 0.85rem; margin: 2px 4px 2px 0; }
.job-id { font-weight: 700; color: #111827; }
.section { margin: 24px 0; }
.toolbar { background: white; border: 1px solid #e5e7eb; border-radius: 10px; padding: 12px; margin-bottom: 16px; }
input[type='text'] { padding: 8px; width: 280px; max-width: 100%; }
button { padding: 8px 12px; }
.pagination { margin-top: 16px; display: flex; gap: 12px; }
.summary-grid { display: grid; grid-template-columns: 220px 1fr; gap: 8px 16px; }
.summary-grid div { padding: 4px 0; }
details { margin: 12px 0; }
a { color: #0f766e; }
</style>
"""
return HTMLResponse(f"<!doctype html><html><head><meta charset='utf-8'><title>{escape(title)}</title>{css}</head><body>{nav}<h1>{escape(title)}</h1>{body}</body></html>")
def pretty_json(value):
try:
if isinstance(value, str):
parsed = json.loads(value)
else:
parsed = value
return json.dumps(parsed, indent=2, ensure_ascii=False)
except Exception:
return str(value)
def html_value(value):
if value is None:
return ""
if isinstance(value, (dict, list)):
return pretty_json(value)
return str(value)
def parse_json_field(value, default=None):
if value is None:
return default
try:
return json.loads(value)
except Exception:
return default
def event_summary(payload):
data = payload.get("data", {}) if isinstance(payload, dict) else {}
related = payload.get("related", {}) if isinstance(payload, dict) else {}
company = related.get("company", {}) if isinstance(related, dict) else {}
job_number = data.get("generated_job_id", "")
purchase_order_number = data.get("purchase_order_number", "")
return {
"event_type": payload.get("type", "") if isinstance(payload, dict) else "",
"uuid": data.get("uuid", ""),
"generated_job_id": job_number,
"job_number": job_number,
"purchase_order_number": purchase_order_number,
"status": data.get("status", ""),
"edit_date": data.get("edit_date", ""),
"company_name": company.get("name", ""),
}
def format_pills(values):
if not values:
return ""
return " ".join(f"<span class='pill'>{escape(str(v))}</span>" for v in values)
def link_with_params(path, **params):
filtered = {k: v for k, v in params.items() if v not in (None, "")}
return f"{path}?{urlencode(filtered)}" if filtered else path
def resolve_generated_job_id(job_uuid: str) -> str:
job_uuid = str(job_uuid or "").strip()
if not job_uuid:
return ""
try:
with closing(get_poll_conn()) as conn:
row = conn.execute(
"select generated_job_id from job_metadata where job_uuid = ?",
(job_uuid,),
).fetchone()
if row and row["generated_job_id"]:
return str(row["generated_job_id"])
except sqlite3.Error:
pass
try:
with closing(get_conn()) as conn:
row = conn.execute(
"""
with jobs as (
select
json_extract(payload_json, '$.data.uuid') as job_uuid,
json_extract(payload_json, '$.data.generated_job_id') as generated_job_id,
received_at
from webhook_events
where json_extract(payload_json, '$.data.generated_job_id') is not null
union all
select
json_extract(payload_json, '$.related.job.uuid') as job_uuid,
json_extract(payload_json, '$.related.job.generated_job_id') as generated_job_id,
received_at
from webhook_form_responses
where json_extract(payload_json, '$.related.job.generated_job_id') is not null
)
select generated_job_id
from jobs
where job_uuid = ?
and generated_job_id is not null
order by received_at desc
limit 1
""",
(job_uuid,),
).fetchone()
if row and row["generated_job_id"]:
return str(row["generated_job_id"])
except sqlite3.Error:
pass
return ""
def job_id_html(job_uuid: str) -> str:
generated_job_id = resolve_generated_job_id(job_uuid)
return f"<span class='job-id'>{escape(generated_job_id)}</span>" if generated_job_id else ""
@app.get("/health")
def health():
return {"ok": True, "db_path": DB_PATH, "state_db_path": STATE_DB_PATH, "poll_db_path": POLL_DB_PATH}
@app.get("/", response_class=HTMLResponse)
def dashboard():
with closing(get_conn()) as conn:
counts = {
"webhook_events": conn.execute("select count(*) from webhook_events").fetchone()[0],
"webhook_objects": conn.execute("select count(*) from webhook_objects").fetchone()[0],
"webhook_form_responses": conn.execute("select count(*) from webhook_form_responses").fetchone()[0],
}
latest_event = conn.execute("select received_at from webhook_events order by id desc limit 1").fetchone()
latest_object = conn.execute("select received_at from webhook_objects order by id desc limit 1").fetchone()
latest_form = conn.execute("select received_at from webhook_form_responses order by id desc limit 1").fetchone()
state_count = 0
latest_generated = None
try:
with closing(get_state_conn()) as conn:
state_count = conn.execute("select count(*) from generated_job_materials").fetchone()[0]
latest_generated = conn.execute("select updated_at from generated_job_materials order by id desc limit 1").fetchone()
except sqlite3.Error:
pass
poll_counts = {
"poll_runs": 0,
"polled_form_responses": 0,
"polled_quote_templates": 0,
"remote_existing_incidents": 0,
}
latest_poll_run = None
latest_polled_form = None
latest_polled_quote = None
latest_remote_existing_incident = None
try:
with closing(get_poll_conn()) as conn:
poll_counts["poll_runs"] = conn.execute("select count(*) from poll_runs").fetchone()[0]
poll_counts["polled_form_responses"] = conn.execute("select count(*) from form_responses_raw").fetchone()[0]
poll_counts["polled_quote_templates"] = conn.execute("select count(*) from quote_template_form_responses").fetchone()[0]
try:
poll_counts["remote_existing_incidents"] = conn.execute("select count(*) from quote_template_remote_existing_incidents").fetchone()[0]
latest_remote_existing_incident = conn.execute("select detected_at from quote_template_remote_existing_incidents order by id desc limit 1").fetchone()
except sqlite3.Error:
pass
latest_poll_run = conn.execute("select finished_at from poll_runs order by id desc limit 1").fetchone()
latest_polled_form = conn.execute("select timestamp from form_responses_raw order by timestamp desc limit 1").fetchone()
latest_polled_quote = conn.execute("select discovered_at from quote_template_form_responses order by discovered_at desc limit 1").fetchone()
except sqlite3.Error:
pass
counts["generated_job_materials"] = state_count
counts.update(poll_counts)
cards = "".join(
f"<div class='card'><div class='muted'>{escape(name)}</div><div class='big'>{count}</div></div>"
for name, count in counts.items()
)
body = f"""
<div class='cards'>{cards}</div>
<div class='card'>
<div class='summary-grid'>
<div><strong>DB path</strong></div><div><code>{escape(DB_PATH)}</code></div>
<div><strong>State DB path</strong></div><div><code>{escape(STATE_DB_PATH)}</code></div>
<div><strong>Poll DB path</strong></div><div><code>{escape(POLL_DB_PATH)}</code></div>
<div><strong>Latest event</strong></div><div>{escape(latest_event[0] if latest_event else '')}</div>
<div><strong>Latest object</strong></div><div>{escape(latest_object[0] if latest_object else '')}</div>
<div><strong>Latest form response</strong></div><div>{escape(latest_form[0] if latest_form else '')}</div>
<div><strong>Latest generated material row</strong></div><div>{escape(latest_generated[0] if latest_generated else '')}</div>
<div><strong>Latest poll run</strong></div><div>{escape(latest_poll_run[0] if latest_poll_run else '')}</div>
<div><strong>Latest polled form timestamp</strong></div><div>{escape(latest_polled_form[0] if latest_polled_form else '')}</div>
<div><strong>Latest polled quote discovered</strong></div><div>{escape(latest_polled_quote[0] if latest_polled_quote else '')}</div>
<div><strong>Latest remote-existing incident</strong></div><div>{escape(latest_remote_existing_incident[0] if latest_remote_existing_incident else '')}</div>
</div>
</div>
<div class='section'>
<h2>Quick links</h2>
<ul>
<li><a href='/events'>Browse webhook events</a></li>
<li><a href='/objects'>Browse object webhooks</a></li>
<li><a href='/form-responses'>Browse webhook form responses</a></li>
<li><a href='/poll/form-responses'>Browse polled form responses</a></li>
<li><a href='/poll/quote-template'>Browse parsed polled Quote Template responses</a></li>
<li><a href='/poll/runs'>Browse poll runs</a></li>
<li><a href='/poll/apply-runs'>Browse dry-run/apply runs</a></li>
<li><a href='/poll/remote-existing-incidents'>Browse remote-existing incidents</a></li>
<li><a href='/generated-materials'>Browse generated job-material state</a></li>
</ul>
</div>
"""
return html_page("ServiceM8 Inspector", body)
@app.get("/events", response_class=HTMLResponse)
def list_events(q: str = Query(""), page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
with closing(get_conn()) as conn:
rows = conn.execute(
"select id, received_at, payload_json, path from webhook_events order by id desc limit ? offset ?",
(PAGE_SIZE, offset),
).fetchall()
filtered = []
q_lower = q.lower().strip()
q_digits = "".join(ch for ch in q if ch.isdigit())
for row in rows:
payload = parse_json_field(row["payload_json"], {}) or {}
summary = event_summary(payload)
job_number = str(summary.get("job_number", "") or "")
purchase_order_number = str(summary.get("purchase_order_number", "") or "")
haystack = " ".join(str(v) for v in summary.values()).lower() + " " + str(payload).lower()
job_match = bool(q_digits) and (q_digits in job_number or q_digits in purchase_order_number)
if q_lower and not job_match and q_lower not in haystack:
continue
filtered.append((row, payload, summary))
table_rows = []
for row, payload, summary in filtered:
table_rows.append(
f"<tr>"
f"<td><a href='/events/{row['id']}'>{row['id']}</a></td>"
f"<td>{escape(row['received_at'])}</td>"
f"<td>{escape(summary['event_type'])}</td>"
f"<td>{escape(summary['generated_job_id'])}</td>"
f"<td>{escape(summary['uuid'])}</td>"
f"<td>{escape(summary['status'])}</td>"
f"<td>{escape(summary['company_name'])}</td>"
f"<td>{escape(row['path'])}</td>"
f"</tr>"
)
body = f"""
<div class='toolbar'>
<form method='get'>
<label>Search <input type='text' name='q' value='{escape(q)}' placeholder='job number, uuid, status, company, type'></label>
<input type='hidden' name='page' value='1'>
<button type='submit'>Filter</button>
</form>
<div class='muted'>Search supports job number / generated job ID, UUID, status, company, or event type. Showing up to {PAGE_SIZE} latest rows, then filtering in-app.</div>
</div>
<table>
<thead><tr><th>ID</th><th>Received</th><th>Type</th><th>Job ID</th><th>UUID</th><th>Status</th><th>Company</th><th>Path</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='8'>No rows matched.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/events', q=q, page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/events', q=q, page=page+1)}'>Next →</a>
</div>
"""
return html_page("Webhook events", body)
@app.get("/events/{event_id}", response_class=HTMLResponse)
def event_detail(event_id: int):
with closing(get_conn()) as conn:
row = conn.execute("select * from webhook_events where id = ?", (event_id,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Event not found")
payload = parse_json_field(row["payload_json"], {}) or {}
headers = parse_json_field(row["headers_json"], {}) or {}
summary = event_summary(payload)
related = payload.get("related", {}) if isinstance(payload, dict) else {}
company = related.get("company", {}) if isinstance(related, dict) else {}
contacts = related.get("jobContacts", []) if isinstance(related, dict) else []
materials = related.get("jobMaterials", []) if isinstance(related, dict) else []
body = f"""
<div class='card summary-grid'>
<div><strong>ID</strong></div><div>{row['id']}</div>
<div><strong>Received</strong></div><div>{escape(row['received_at'])}</div>
<div><strong>Event type</strong></div><div>{escape(summary['event_type'])}</div>
<div><strong>UUID</strong></div><div>{escape(summary['uuid'])}</div>
<div><strong>Generated job ID</strong></div><div>{escape(summary['generated_job_id'])}</div>
<div><strong>Status</strong></div><div>{escape(summary['status'])}</div>
<div><strong>Company</strong></div><div>{escape(summary['company_name'])}</div>
<div><strong>Path</strong></div><div>{escape(row['path'])}</div>
</div>
<div class='section'>
<h2>Related summary</h2>
<div class='card summary-grid'>
<div><strong>Company name</strong></div><div>{escape(company.get('name', ''))}</div>
<div><strong>Contacts</strong></div><div>{len(contacts)}</div>
<div><strong>Materials</strong></div><div>{len(materials)}</div>
</div>
</div>
<div class='section'>
<h2>Headers</h2>
<pre>{escape(pretty_json(headers))}</pre>
</div>
<div class='section'>
<h2>Payload</h2>
<pre>{escape(pretty_json(payload))}</pre>
</div>
"""
return html_page(f"Event {event_id}", body)
@app.get("/objects", response_class=HTMLResponse)
def list_objects(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
with closing(get_conn()) as conn:
rows = conn.execute(
"select id, received_at, object_type, object_uuid, changed_fields_json, object_time_utc, resource_url from webhook_objects order by id desc limit ? offset ?",
(PAGE_SIZE, offset),
).fetchall()
table_rows = []
for row in rows:
changed = parse_json_field(row["changed_fields_json"], []) or []
table_rows.append(
f"<tr>"
f"<td><a href='/objects/{row['id']}'>{row['id']}</a></td>"
f"<td>{escape(row['received_at'])}</td>"
f"<td>{escape(row['object_type'] or '')}</td>"
f"<td>{escape(row['object_uuid'] or '')}</td>"
f"<td>{format_pills(changed[:8])}</td>"
f"<td>{escape(row['object_time_utc'] or '')}</td>"
f"<td><a href='{escape(row['resource_url'] or '')}'>{escape(row['resource_url'] or '')}</a></td>"
f"</tr>"
)
body = f"""
<table>
<thead><tr><th>ID</th><th>Received</th><th>Object</th><th>UUID</th><th>Changed fields</th><th>Object time</th><th>Resource URL</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='7'>No rows found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/objects', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/objects', page=page+1)}'>Next →</a>
</div>
"""
return html_page("Object webhooks", body)
@app.get("/objects/{object_id}", response_class=HTMLResponse)
def object_detail(object_id: int):
with closing(get_conn()) as conn:
row = conn.execute("select * from webhook_objects where id = ?", (object_id,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Object row not found")
payload = parse_json_field(row["payload_json"], {}) or {}
headers = parse_json_field(row["headers_json"], {}) or {}
changed = parse_json_field(row["changed_fields_json"], []) or []
body = f"""
<div class='card summary-grid'>
<div><strong>ID</strong></div><div>{row['id']}</div>
<div><strong>Received</strong></div><div>{escape(row['received_at'])}</div>
<div><strong>Object type</strong></div><div>{escape(row['object_type'] or '')}</div>
<div><strong>Object UUID</strong></div><div>{escape(row['object_uuid'] or '')}</div>
<div><strong>Object time UTC</strong></div><div>{escape(row['object_time_utc'] or '')}</div>
<div><strong>Resource URL</strong></div><div><a href='{escape(row['resource_url'] or '')}'>{escape(row['resource_url'] or '')}</a></div>
<div><strong>Changed fields</strong></div><div>{format_pills(changed)}</div>
</div>
<div class='section'><h2>Headers</h2><pre>{escape(pretty_json(headers))}</pre></div>
<div class='section'><h2>Payload</h2><pre>{escape(pretty_json(payload))}</pre></div>
"""
return html_page(f"Object webhook {object_id}", body)
@app.get("/form-responses", response_class=HTMLResponse)
def list_form_responses(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
with closing(get_conn()) as conn:
rows = conn.execute(
"select id, received_at, payload_json from webhook_form_responses order by id desc limit ? offset ?",
(PAGE_SIZE, offset),
).fetchall()
table_rows = []
for row in rows:
payload = parse_json_field(row["payload_json"], {}) or {}
data = payload.get("data", {}) if isinstance(payload, dict) else {}
related = payload.get("related", {}) if isinstance(payload, dict) else {}
job = related.get("job", {}) if isinstance(related, dict) else {}
table_rows.append(
f"<tr>"
f"<td><a href='/form-responses/{row['id']}'>{row['id']}</a></td>"
f"<td>{escape(row['received_at'])}</td>"
f"<td>{escape(data.get('form_uuid', ''))}</td>"
f"<td>{escape(data.get('regarding_object', ''))}</td>"
f"<td>{escape(data.get('regarding_object_uuid', ''))}</td>"
f"<td>{escape(job.get('generated_job_id', ''))}</td>"
f"<td>{escape(job.get('status', ''))}</td>"
f"</tr>"
)
body = f"""
<table>
<thead><tr><th>ID</th><th>Received</th><th>Form UUID</th><th>Regarding</th><th>Object UUID</th><th>Job ID</th><th>Job status</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='7'>No rows found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/form-responses', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/form-responses', page=page+1)}'>Next →</a>
</div>
"""
return html_page("Form responses", body)
@app.get("/generated-materials", response_class=HTMLResponse)
def list_generated_materials(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
try:
with closing(get_state_conn()) as conn:
rows = conn.execute(
"select id, job_uuid, form_response_uuid, job_material_uuid, kind, source_question, source_text, updated_at from generated_job_materials order by id desc limit ? offset ?",
(PAGE_SIZE, offset),
).fetchall()
except sqlite3.Error as e:
return html_page("Generated materials", f"<div class='card'>State DB unavailable: {escape(str(e))}</div>")
table_rows = []
for row in rows:
table_rows.append(
f"<tr>"
f"<td><a href='/generated-materials/{row['id']}'>{row['id']}</a></td>"
f"<td>{job_id_html(row['job_uuid'])}</td>"
f"<td>{escape(row['job_uuid'] or '')}</td>"
f"<td>{escape(row['form_response_uuid'] or '')}</td>"
f"<td>{escape(row['job_material_uuid'] or '')}</td>"
f"<td>{escape(row['kind'] or '')}</td>"
f"<td>{escape(row['source_question'] or '')}</td>"
f"<td>{escape(row['updated_at'] or '')}</td>"
f"</tr>"
)
body = f"""
<table>
<thead><tr><th>ID</th><th>Job ID</th><th>Job UUID</th><th>Form response UUID</th><th>Job material UUID</th><th>Kind</th><th>Source question</th><th>Updated</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='8'>No rows found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/generated-materials', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/generated-materials', page=page+1)}'>Next →</a>
</div>
"""
return html_page("Generated materials", body)
@app.get("/generated-materials/{row_id}", response_class=HTMLResponse)
def generated_material_detail(row_id: int):
try:
with closing(get_state_conn()) as conn:
row = conn.execute("select * from generated_job_materials where id = ?", (row_id,)).fetchone()
except sqlite3.Error as e:
return html_page("Generated material detail", f"<div class='card'>State DB unavailable: {escape(str(e))}</div>")
if not row:
raise HTTPException(status_code=404, detail="Generated material row not found")
body = f"""
<div class='card summary-grid'>
<div><strong>ID</strong></div><div>{row['id']}</div>
<div><strong>Job ID</strong></div><div>{job_id_html(row['job_uuid'])}</div>
<div><strong>Job UUID</strong></div><div>{escape(row['job_uuid'] or '')}</div>
<div><strong>Form response UUID</strong></div><div>{escape(row['form_response_uuid'] or '')}</div>
<div><strong>Job material UUID</strong></div><div>{escape(row['job_material_uuid'] or '')}</div>
<div><strong>Kind</strong></div><div>{escape(row['kind'] or '')}</div>
<div><strong>Source field UUID</strong></div><div>{escape(row['source_field_uuid'] or '')}</div>
<div><strong>Source question</strong></div><div>{escape(row['source_question'] or '')}</div>
<div><strong>Source text</strong></div><div>{escape(row['source_text'] or '')}</div>
<div><strong>Created</strong></div><div>{escape(row['created_at'] or '')}</div>
<div><strong>Updated</strong></div><div>{escape(row['updated_at'] or '')}</div>
</div>
"""
return html_page(f"Generated material {row_id}", body)
@app.get("/form-responses/{row_id}", response_class=HTMLResponse)
def form_response_detail(row_id: int):
with closing(get_conn()) as conn:
row = conn.execute("select * from webhook_form_responses where id = ?", (row_id,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Form response not found")
payload = parse_json_field(row["payload_json"], {}) or {}
headers = parse_json_field(row["headers_json"], {}) or {}
data = payload.get("data", {}) if isinstance(payload, dict) else {}
related = payload.get("related", {}) if isinstance(payload, dict) else {}
job = related.get("job", {}) if isinstance(related, dict) else {}
field_data_raw = data.get("field_data", "[]")
field_data = parse_json_field(field_data_raw, []) or []
field_rows = []
for item in sorted(field_data, key=lambda x: x.get("SortOrder", 0)):
field_rows.append(
f"<tr>"
f"<td>{escape(html_value(item.get('SortOrder')))}</td>"
f"<td>{escape(html_value(item.get('FieldType')))}</td>"
f"<td>{escape(html_value(item.get('Question')))}</td>"
f"<td>{escape(html_value(item.get('Response')))}</td>"
f"</tr>"
)
body = f"""
<div class='card summary-grid'>
<div><strong>ID</strong></div><div>{row['id']}</div>
<div><strong>Received</strong></div><div>{escape(row['received_at'])}</div>
<div><strong>Form UUID</strong></div><div>{escape(data.get('form_uuid', ''))}</div>
<div><strong>Regarding object</strong></div><div>{escape(data.get('regarding_object', ''))}</div>
<div><strong>Regarding UUID</strong></div><div>{escape(data.get('regarding_object_uuid', ''))}</div>
<div><strong>Timestamp</strong></div><div>{escape(data.get('timestamp', ''))}</div>
<div><strong>Job ID</strong></div><div>{escape(job.get('generated_job_id', ''))}</div>
<div><strong>Job status</strong></div><div>{escape(job.get('status', ''))}</div>
</div>
<div class='section'>
<h2>Decoded field data</h2>
<table>
<thead><tr><th>Order</th><th>Type</th><th>Question</th><th>Response</th></tr></thead>
<tbody>{''.join(field_rows) or "<tr><td colspan='4'>No decoded field data.</td></tr>"}</tbody>
</table>
</div>
<div class='section'><h2>Related job</h2><pre>{escape(pretty_json(job))}</pre></div>
<div class='section'><h2>Headers</h2><pre>{escape(pretty_json(headers))}</pre></div>
<div class='section'><h2>Payload</h2><pre>{escape(pretty_json(payload))}</pre></div>
"""
return html_page(f"Form response {row_id}", body)
@app.get("/poll/remote-existing-incidents", response_class=HTMLResponse)
def list_remote_existing_incidents(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
try:
with closing(get_poll_conn()) as conn:
rows = conn.execute(
"""
select id, detected_at, form_response_uuid, job_uuid, apply_run_id,
desired_count, remote_count, remote_active_count, action, reason
from quote_template_remote_existing_incidents
order by id desc
limit ? offset ?
""",
(PAGE_SIZE, offset),
).fetchall()
except sqlite3.Error as e:
return html_page("Remote existing incidents", f"<div class='card'>Incident table unavailable: {escape(str(e))}</div>")
table_rows = []
for row in rows:
run_link = f"<a href='/poll/apply-runs/{row['apply_run_id']}'>{row['apply_run_id']}</a>" if row['apply_run_id'] else ""
table_rows.append(
f"<tr><td><a href='/poll/remote-existing-incidents/{row['id']}'>{row['id']}</a></td>"
f"<td>{escape(row['detected_at'] or '')}</td><td>{escape(row['action'] or '')}</td>"
f"<td><a href='/poll/quote-template/{escape(row['form_response_uuid'])}'>{escape(row['form_response_uuid'])}</a></td>"
f"<td>{job_id_html(row['job_uuid'])}</td><td>{escape(row['job_uuid'] or '')}</td><td>{run_link}</td>"
f"<td>{row['desired_count']}</td><td>{row['remote_count']}</td><td>{row['remote_active_count']}</td>"
f"<td>{escape((row['reason'] or '')[:180])}</td></tr>"
)
body = f"""
<table>
<thead><tr><th>ID</th><th>Detected</th><th>Action</th><th>Form response UUID</th><th>Job ID</th><th>Job UUID</th><th>Apply run</th><th>Desired</th><th>Remote rows</th><th>Active</th><th>Reason</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='11'>No remote-existing incidents found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/poll/remote-existing-incidents', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/poll/remote-existing-incidents', page=page+1)}'>Next →</a>
</div>
"""
return html_page("Remote existing incidents", body)
@app.get("/poll/remote-existing-incidents/{incident_id}", response_class=HTMLResponse)
def remote_existing_incident_detail(incident_id: int):
try:
with closing(get_poll_conn()) as conn:
row = conn.execute("select * from quote_template_remote_existing_incidents where id = ?", (incident_id,)).fetchone()
except sqlite3.Error as e:
return html_page("Remote existing incident", f"<div class='card'>Incident table unavailable: {escape(str(e))}</div>")
if not row:
raise HTTPException(status_code=404, detail="Remote existing incident not found")
remote_rows = parse_json_field(row["remote_rows_json"], []) or []
material_rows = []
if isinstance(remote_rows, list):
for item in remote_rows:
if isinstance(item, dict):
material_rows.append(
f"<tr><td>{escape(str(item.get('uuid', '')))}</td><td>{escape(str(item.get('active', '')))}</td>"
f"<td>{escape(str(item.get('name', '')))}</td><td>{escape(str(item.get('material_uuid', '')))}</td>"
f"<td>{escape(str(item.get('quantity', '')))}</td><td>{escape(str(item.get('price', '')))}</td>"
f"<td>{escape(str(item.get('sort_order', '')))}</td></tr>"
)
run_link = f"<a href='/poll/apply-runs/{row['apply_run_id']}'>{row['apply_run_id']}</a>" if row['apply_run_id'] else ""
body = f"""
<div class='card summary-grid'>
<div><strong>ID</strong></div><div>{row['id']}</div>
<div><strong>Detected</strong></div><div>{escape(row['detected_at'] or '')}</div>
<div><strong>Action</strong></div><div>{escape(row['action'] or '')}</div>
<div><strong>Form response UUID</strong></div><div><a href='/poll/quote-template/{escape(row['form_response_uuid'])}'>{escape(row['form_response_uuid'])}</a></div>
<div><strong>Job ID</strong></div><div>{job_id_html(row['job_uuid'])}</div>
<div><strong>Job UUID</strong></div><div>{escape(row['job_uuid'] or '')}</div>
<div><strong>Apply run</strong></div><div>{run_link}</div>
<div><strong>Desired rows</strong></div><div>{row['desired_count']}</div>
<div><strong>Remote rows</strong></div><div>{row['remote_count']}</div>
<div><strong>Remote active rows</strong></div><div>{row['remote_active_count']}</div>
<div><strong>Reason</strong></div><div>{escape(row['reason'] or '')}</div>
</div>
<div class='section'><h2>Remote jobMaterial rows</h2><table><thead><tr><th>UUID</th><th>Active</th><th>Name</th><th>Material UUID</th><th>Qty</th><th>Price</th><th>Sort</th></tr></thead><tbody>{''.join(material_rows) or "<tr><td colspan='7'>No remote rows captured.</td></tr>"}</tbody></table></div>
<div class='section'><h2>Raw remote rows JSON</h2><pre>{escape(pretty_json(remote_rows))}</pre></div>
"""
return html_page(f"Remote existing incident {incident_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"<div class='card'>Apply-run table unavailable: {escape(str(e))}</div>")
table_rows = []
for row in rows:
table_rows.append(
f"<tr><td><a href='/poll/apply-runs/{row['id']}'>{row['id']}</a></td>"
f"<td>{escape(row['mode'] or '')}</td><td>{escape(row['status'] or '')}</td>"
f"<td><a href='/poll/quote-template/{escape(row['form_response_uuid'])}'>{escape(row['form_response_uuid'])}</a></td>"
f"<td>{job_id_html(row['job_uuid'])}</td><td>{escape(row['job_uuid'] or '')}</td><td>{escape(row['started_at'] or '')}</td><td>{escape(row['finished_at'] or '')}</td>"
f"<td>{row['desired_count']}</td><td>{row['created_count']}</td><td>{escape((row['error'] or '')[:160])}</td></tr>"
)
body = f"""
<table>
<thead><tr><th>ID</th><th>Mode</th><th>Status</th><th>Form response UUID</th><th>Job ID</th><th>Job UUID</th><th>Started</th><th>Finished</th><th>Desired</th><th>Created</th><th>Error</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='11'>No apply runs found yet.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/poll/apply-runs', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/poll/apply-runs', page=page+1)}'>Next →</a>
</div>
"""
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()
incidents = conn.execute(
"select id, detected_at, action, remote_count, remote_active_count, reason from quote_template_remote_existing_incidents where apply_run_id = ? order by id desc",
(run_id,),
).fetchall()
except sqlite3.Error as e:
return html_page("Apply run", f"<div class='card'>Apply-run table unavailable: {escape(str(e))}</div>")
if not run:
raise HTTPException(status_code=404, detail="Apply run not found")
incident_rows = []
for incident in incidents:
incident_rows.append(
f"<tr><td><a href='/poll/remote-existing-incidents/{incident['id']}'>{incident['id']}</a></td>"
f"<td>{escape(incident['detected_at'] or '')}</td><td>{escape(incident['action'] or '')}</td>"
f"<td>{incident['remote_count']}</td><td>{incident['remote_active_count']}</td><td>{escape((incident['reason'] or '')[:180])}</td></tr>"
)
table_rows = []
for row in rows:
payload = parse_json_field(row['api_payload_json'], {}) or {}
table_rows.append(
f"<tr><td>{row['row_index']}</td><td>{escape(row['action'] or '')}</td><td>{escape(row['kind'] or '')}</td>"
f"<td>{escape(row['name'] or '')}</td><td>{escape(row['job_material_uuid'] or '')}</td>"
f"<td>{escape(row['source_question'] or '')}</td><td>{escape(row['error'] or '')}</td></tr>"
f"<tr><td></td><td colspan='6'><details><summary>API payload</summary><pre>{escape(pretty_json(payload))}</pre></details></td></tr>"
)
body = f"""
<div class='card summary-grid'>
<div><strong>Run ID</strong></div><div>{run['id']}</div>
<div><strong>Mode</strong></div><div>{escape(run['mode'] or '')}</div>
<div><strong>Status</strong></div><div>{escape(run['status'] or '')}</div>
<div><strong>Form response UUID</strong></div><div><a href='/poll/quote-template/{escape(run['form_response_uuid'])}'>{escape(run['form_response_uuid'])}</a></div>
<div><strong>Job ID</strong></div><div>{job_id_html(run['job_uuid'])}</div>
<div><strong>Job UUID</strong></div><div>{escape(run['job_uuid'] or '')}</div>
<div><strong>Started</strong></div><div>{escape(run['started_at'] or '')}</div>
<div><strong>Finished</strong></div><div>{escape(run['finished_at'] or '')}</div>
<div><strong>Desired</strong></div><div>{run['desired_count']}</div>
<div><strong>Created</strong></div><div>{run['created_count']}</div>
<div><strong>Error</strong></div><div>{escape(run['error'] or '')}</div>
</div>
<div class='section'><h2>Remote-existing incidents</h2><table><thead><tr><th>ID</th><th>Detected</th><th>Action</th><th>Remote rows</th><th>Active</th><th>Reason</th></tr></thead><tbody>{''.join(incident_rows) or "<tr><td colspan='6'>No remote-existing incidents for this run.</td></tr>"}</tbody></table></div>
<div class='section'><h2>Rows</h2><table><thead><tr><th>#</th><th>Action</th><th>Kind</th><th>Name</th><th>Created UUID</th><th>Source question</th><th>Error</th></tr></thead><tbody>{''.join(table_rows) or "<tr><td colspan='7'>No rows recorded.</td></tr>"}</tbody></table></div>
"""
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
try:
with closing(get_poll_conn()) as conn:
rows = conn.execute(
"""
select id, started_at, finished_at, since_value, filter_field,
fetched_count, inserted_count, updated_count,
quote_match_count, newly_queued_count, error
from poll_runs order by id desc limit ? offset ?
""",
(PAGE_SIZE, offset),
).fetchall()
except sqlite3.Error as e:
return html_page("Poll runs", f"<div class='card'>Poll DB unavailable: {escape(str(e))}</div>")
table_rows = []
for row in rows:
table_rows.append(
f"<tr><td>{row['id']}</td><td>{escape(row['started_at'] or '')}</td>"
f"<td>{escape(row['finished_at'] or '')}</td>"
f"<td>{escape(row['filter_field'] or '')} gt {escape(row['since_value'] or '')}</td>"
f"<td>{row['fetched_count']}</td><td>{row['inserted_count']}</td><td>{row['updated_count']}</td>"
f"<td>{row['quote_match_count']}</td><td>{row['newly_queued_count']}</td>"
f"<td>{escape((row['error'] or '')[:180])}</td></tr>"
)
body = f"""
<table>
<thead><tr><th>ID</th><th>Started</th><th>Finished</th><th>Filter</th><th>Fetched</th><th>Inserted</th><th>Updated</th><th>Quote matches</th><th>Queued</th><th>Error</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='10'>No poll runs found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/poll/runs', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/poll/runs', page=page+1)}'>Next →</a>
</div>
"""
return html_page("Poll runs", body)
@app.get("/poll/form-responses", response_class=HTMLResponse)
def list_polled_form_responses(q: str = Query(""), quote_only: int = Query(0), page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
clauses = []
params = []
if quote_only:
clauses.append("is_quote_template = 1")
if q.strip():
like = f"%{q.strip()}%"
clauses.append("(uuid like ? or form_uuid like ? or regarding_object_uuid like ? or timestamp like ? or edit_date like ? or raw_json like ?)")
params.extend([like, like, like, like, like, like])
where = " where " + " and ".join(clauses) if clauses else ""
try:
with closing(get_poll_conn()) as conn:
rows = conn.execute(
f"""
select uuid, first_seen_at, last_seen_at, seen_count, timestamp, edit_date,
form_uuid, regarding_object, regarding_object_uuid,
is_quote_template, parse_status, parse_error
from form_responses_raw {where}
order by timestamp desc, edit_date desc limit ? offset ?
""",
(*params, PAGE_SIZE, offset),
).fetchall()
except sqlite3.Error as e:
return html_page("Polled form responses", f"<div class='card'>Poll DB unavailable: {escape(str(e))}</div>")
table_rows = []
for row in rows:
quote_pill = "<span class='pill'>Quote Template</span>" if row["is_quote_template"] else ""
table_rows.append(
f"<tr><td><a href='/poll/form-responses/{escape(row['uuid'])}'>{escape(row['uuid'])}</a></td>"
f"<td>{escape(row['timestamp'] or '')}</td><td>{escape(row['edit_date'] or '')}</td>"
f"<td>{escape(row['form_uuid'] or '')}<br>{quote_pill}</td>"
f"<td>{escape(row['regarding_object'] or '')}</td><td>{job_id_html(row['regarding_object_uuid'])}</td><td>{escape(row['regarding_object_uuid'] or '')}</td>"
f"<td>{escape(row['parse_status'] or '')}</td><td>{row['seen_count']}</td><td>{escape(row['last_seen_at'] or '')}</td></tr>"
)
body = f"""
<div class='toolbar'>
<form method='get'>
<label>Search <input type='text' name='q' value='{escape(q)}' placeholder='uuid, job uuid, form uuid, timestamp'></label>
<label><input type='checkbox' name='quote_only' value='1' {'checked' if quote_only else ''}> Quote Template only</label>
<input type='hidden' name='page' value='1'>
<button type='submit'>Filter</button>
</form>
</div>
<table>
<thead><tr><th>UUID</th><th>Timestamp</th><th>Edit date</th><th>Form UUID</th><th>Regarding</th><th>Job ID</th><th>Object UUID</th><th>Parse</th><th>Seen</th><th>Last seen</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='10'>No rows found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/poll/form-responses', q=q, quote_only=quote_only, page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/poll/form-responses', q=q, quote_only=quote_only, page=page+1)}'>Next →</a>
</div>
"""
return html_page("Polled form responses", body)
@app.get("/poll/form-responses/{form_response_uuid}", response_class=HTMLResponse)
def polled_form_response_detail(form_response_uuid: str):
try:
with closing(get_poll_conn()) as conn:
row = conn.execute("select * from form_responses_raw where uuid = ?", (form_response_uuid,)).fetchone()
quote = conn.execute("select * from quote_template_form_responses where form_response_uuid = ?", (form_response_uuid,)).fetchone()
except sqlite3.Error as e:
return html_page("Polled form response", f"<div class='card'>Poll DB unavailable: {escape(str(e))}</div>")
if not row:
raise HTTPException(status_code=404, detail="Polled form response not found")
raw = parse_json_field(row["raw_json"], {}) or {}
field_data = parse_json_field(raw.get("field_data"), []) or []
field_rows = []
if isinstance(field_data, list):
for item in sorted(field_data, key=lambda x: x.get("SortOrder", 0) if isinstance(x, dict) else 0):
if isinstance(item, dict):
field_rows.append(
f"<tr><td>{escape(html_value(item.get('SortOrder')))}</td><td>{escape(html_value(item.get('FieldType')))}</td>"
f"<td>{escape(html_value(item.get('Question')))}</td><td>{escape(html_value(item.get('Response')))}</td></tr>"
)
quote_link = f"<div><strong>Parsed quote</strong></div><div><a href='/poll/quote-template/{escape(form_response_uuid)}'>Open parsed Quote Template view</a></div>" if quote else ""
body = f"""
<div class='card summary-grid'>
<div><strong>UUID</strong></div><div>{escape(row['uuid'])}</div>
<div><strong>Timestamp</strong></div><div>{escape(row['timestamp'] or '')}</div>
<div><strong>Edit date</strong></div><div>{escape(row['edit_date'] or '')}</div>
<div><strong>Form UUID</strong></div><div>{escape(row['form_uuid'] or '')}</div>
<div><strong>Regarding object</strong></div><div>{escape(row['regarding_object'] or '')}</div>
<div><strong>Job ID</strong></div><div>{job_id_html(row['regarding_object_uuid'])}</div>
<div><strong>Regarding UUID</strong></div><div>{escape(row['regarding_object_uuid'] or '')}</div>
<div><strong>First seen</strong></div><div>{escape(row['first_seen_at'] or '')}</div>
<div><strong>Last seen</strong></div><div>{escape(row['last_seen_at'] or '')}</div>
<div><strong>Seen count</strong></div><div>{row['seen_count']}</div>
<div><strong>Parse status</strong></div><div>{escape(row['parse_status'] or '')}</div>
<div><strong>Parse error</strong></div><div>{escape(row['parse_error'] or '')}</div>
{quote_link}
</div>
<div class='section'><h2>Decoded field data</h2><table><thead><tr><th>Order</th><th>Type</th><th>Question</th><th>Response</th></tr></thead><tbody>{''.join(field_rows) or "<tr><td colspan='4'>No decoded field data.</td></tr>"}</tbody></table></div>
<div class='section'><h2>Raw API row</h2><pre>{escape(pretty_json(raw))}</pre></div>
"""
return html_page(f"Polled form response {form_response_uuid}", body)
@app.get("/poll/quote-template", response_class=HTMLResponse)
def list_polled_quote_templates(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE
try:
with closing(get_poll_conn()) as conn:
rows = conn.execute(
"""
select form_response_uuid, discovered_at, job_uuid, form_uuid, author_name,
description, desired_job_materials_json, queued_at, processed_at,
process_status, process_error
from quote_template_form_responses
order by discovered_at desc, form_response_uuid desc limit ? offset ?
""",
(PAGE_SIZE, offset),
).fetchall()
except sqlite3.Error as e:
return html_page("Polled Quote Template responses", f"<div class='card'>Poll DB unavailable: {escape(str(e))}</div>")
table_rows = []
for row in rows:
try:
material_count = len(json.loads(row["desired_job_materials_json"] or "[]"))
except Exception:
material_count = "?"
table_rows.append(
f"<tr><td><a href='/poll/quote-template/{escape(row['form_response_uuid'])}'>{escape(row['form_response_uuid'])}</a></td>"
f"<td>{escape(row['discovered_at'] or '')}</td><td>{job_id_html(row['job_uuid'])}</td><td>{escape(row['job_uuid'] or '')}</td>"
f"<td>{escape(row['description'] or '')}</td><td>{material_count}</td>"
f"<td>{escape(row['queued_at'] or '')}</td><td>{escape(row['process_status'] or '')}</td></tr>"
)
body = f"""
<table>
<thead><tr><th>Form response UUID</th><th>Discovered</th><th>Job ID</th><th>Job UUID</th><th>Description</th><th>Rows</th><th>Queued</th><th>Status</th></tr></thead>
<tbody>{''.join(table_rows) or "<tr><td colspan='8'>No quote template rows found.</td></tr>"}</tbody>
</table>
<div class='pagination'>
{f"<a href='{link_with_params('/poll/quote-template', page=page-1)}'>← Prev</a>" if page > 1 else ''}
<a href='{link_with_params('/poll/quote-template', page=page+1)}'>Next →</a>
</div>
"""
return html_page("Polled Quote Template responses", body)
@app.get("/poll/quote-template/{form_response_uuid}", response_class=HTMLResponse)
def polled_quote_template_detail(form_response_uuid: str):
try:
with closing(get_poll_conn()) as conn:
row = conn.execute("select * from quote_template_form_responses where form_response_uuid = ?", (form_response_uuid,)).fetchone()
except sqlite3.Error as e:
return html_page("Polled Quote Template response", f"<div class='card'>Poll DB unavailable: {escape(str(e))}</div>")
if not row:
raise HTTPException(status_code=404, detail="Polled quote template response not found")
parsed = parse_json_field(row["parsed_json"], {}) or {}
materials = parse_json_field(row["desired_job_materials_json"], []) or []
material_rows = []
for item in materials:
material_rows.append(
f"<tr><td>{escape(str(item.get('sort_order', '')))}</td><td>{escape(item.get('kind', ''))}</td>"
f"<td>{escape(item.get('name', ''))}</td><td>{escape(item.get('material_uuid', ''))}</td>"
f"<td>{escape(item.get('source_question', ''))}</td></tr>"
)
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()
recent_incidents = conn.execute(
"select id, detected_at, action, remote_count, remote_active_count, reason from quote_template_remote_existing_incidents where form_response_uuid = ? order by id desc limit 8",
(row['form_response_uuid'],),
).fetchall()
except sqlite3.Error:
recent_runs = []
recent_incidents = []
recent_run_rows = []
for run in recent_runs:
recent_run_rows.append(
f"<tr><td><a href='/poll/apply-runs/{run['id']}'>{run['id']}</a></td><td>{escape(run['mode'] or '')}</td><td>{escape(run['status'] or '')}</td><td>{escape(run['started_at'] or '')}</td><td>{escape(run['finished_at'] or '')}</td><td>{run['desired_count']}</td><td>{run['created_count']}</td><td>{escape((run['error'] or '')[:120])}</td></tr>"
)
recent_incident_rows = []
for incident in recent_incidents:
recent_incident_rows.append(
f"<tr><td><a href='/poll/remote-existing-incidents/{incident['id']}'>{incident['id']}</a></td><td>{escape(incident['detected_at'] or '')}</td><td>{escape(incident['action'] or '')}</td><td>{incident['remote_count']}</td><td>{incident['remote_active_count']}</td><td>{escape((incident['reason'] or '')[:120])}</td></tr>"
)
body = f"""
<div class='card summary-grid'>
<div><strong>Form response UUID</strong></div><div>{escape(row['form_response_uuid'])}</div>
<div><strong>Discovered</strong></div><div>{escape(row['discovered_at'] or '')}</div>
<div><strong>Job ID</strong></div><div>{job_id_html(row['job_uuid'])}</div>
<div><strong>Job UUID</strong></div><div>{escape(row['job_uuid'] or '')}</div>
<div><strong>Form UUID</strong></div><div>{escape(row['form_uuid'] or '')}</div>
<div><strong>Author</strong></div><div>{escape(row['author_name'] or '')}</div>
<div><strong>Description</strong></div><div>{escape(row['description'] or '')}</div>
<div><strong>Queued</strong></div><div>{escape(row['queued_at'] or '')}</div>
<div><strong>Processed</strong></div><div>{escape(row['processed_at'] or '')}</div>
<div><strong>Status</strong></div><div>{escape(row['process_status'] or '')}</div>
<div><strong>Error</strong></div><div>{escape(row['process_error'] or '')}</div>
<div><strong>Raw polled response</strong></div><div><a href='/poll/form-responses/{escape(row['form_response_uuid'])}'>Open raw polled form response</a></div>
</div>
<div class='section'>
<h2>Selective apply commands</h2>
<div class='card'>
<p><strong>Dry-run first:</strong></p>
<pre>{escape(dry_run_cmd)}</pre>
<p><strong>Apply to ServiceM8 only after checking the dry-run:</strong></p>
<pre>{escape(apply_cmd)}</pre>
</div>
</div>
<div class='section'>
<h2>Recent dry-run/apply runs for this response</h2>
<table><thead><tr><th>ID</th><th>Mode</th><th>Status</th><th>Started</th><th>Finished</th><th>Desired</th><th>Created</th><th>Error</th></tr></thead><tbody>{''.join(recent_run_rows) or "<tr><td colspan='8'>No dry-run/apply runs yet.</td></tr>"}</tbody></table>
</div>
<div class='section'>
<h2>Remote-existing incidents for this response</h2>
<table><thead><tr><th>ID</th><th>Detected</th><th>Action</th><th>Remote rows</th><th>Active</th><th>Reason</th></tr></thead><tbody>{''.join(recent_incident_rows) or "<tr><td colspan='6'>No remote-existing incidents yet.</td></tr>"}</tbody></table>
</div>
<div class='section'><h2>Desired jobMaterial rows</h2><table><thead><tr><th>Sort</th><th>Kind</th><th>Name</th><th>Material UUID</th><th>Source question</th></tr></thead><tbody>{''.join(material_rows) or "<tr><td colspan='5'>No desired jobMaterial rows.</td></tr>"}</tbody></table></div>
<div class='section'><h2>Parsed JSON</h2><pre>{escape(pretty_json(parsed))}</pre></div>
"""
return html_page(f"Polled Quote Template {form_response_uuid}", body)
if __name__ == "__main__":
import uvicorn
uvicorn.run("servicem8_inspector:app", host=APP_HOST, port=APP_PORT, reload=False)