quick backup

This commit is contained in:
2026-05-04 13:14:44 +10:00
parent 89386909a1
commit 7e3538e745
+114
View File
@@ -46,6 +46,7 @@ def html_page(title: str, body: str) -> HTMLResponse:
<a href='/form-responses'>Webhook form responses</a> <a href='/form-responses'>Webhook form responses</a>
<a href='/poll/form-responses'>Polled form responses</a> <a href='/poll/form-responses'>Polled form responses</a>
<a href='/poll/quote-template'>Polled quote templates</a> <a href='/poll/quote-template'>Polled quote templates</a>
<a href='/poll/apply-runs'>Apply runs</a>
<a href='/generated-materials'>Generated materials</a> <a href='/generated-materials'>Generated materials</a>
</nav> </nav>
""" """
@@ -206,6 +207,7 @@ def dashboard():
<li><a href='/poll/form-responses'>Browse polled 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/quote-template'>Browse parsed polled Quote Template responses</a></li>
<li><a href='/poll/runs'>Browse poll runs</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='/generated-materials'>Browse generated job-material state</a></li> <li><a href='/generated-materials'>Browse generated job-material state</a></li>
</ul> </ul>
</div> </div>
@@ -540,6 +542,89 @@ def form_response_detail(row_id: int):
return html_page(f"Form response {row_id}", body) 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"<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>{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 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='10'>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()
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")
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 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>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) @app.get("/poll/runs", response_class=HTMLResponse)
def list_poll_runs(page: int = Query(1, ge=1)): def list_poll_runs(page: int = Query(1, ge=1)):
offset = (page - 1) * PAGE_SIZE offset = (page - 1) * PAGE_SIZE
@@ -749,6 +834,22 @@ def polled_quote_template_detail(form_response_uuid: str):
f"<td>{escape(item.get('source_question', ''))}</td></tr>" 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()
except sqlite3.Error:
recent_runs = []
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>"
)
body = f""" body = f"""
<div class='card summary-grid'> <div class='card summary-grid'>
<div><strong>Form response UUID</strong></div><div>{escape(row['form_response_uuid'])}</div> <div><strong>Form response UUID</strong></div><div>{escape(row['form_response_uuid'])}</div>
@@ -763,6 +864,19 @@ def polled_quote_template_detail(form_response_uuid: str):
<div><strong>Error</strong></div><div>{escape(row['process_error'] 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><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>
<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>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>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> <div class='section'><h2>Parsed JSON</h2><pre>{escape(pretty_json(parsed))}</pre></div>
""" """