Added the creation of jobmaterials script and the creation script ofr live updates to the ServiceM8 server (default is -dry-run). Altered inspector.py to now let us view jobmaterials DB.

This commit is contained in:
2026-04-28 16:41:06 +10:00
parent debc442081
commit 3472e770d6
3 changed files with 336 additions and 3 deletions
+90 -1
View File
@@ -10,6 +10,7 @@ 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")
APP_HOST = os.getenv("WEBHOOK_INSPECTOR_HOST", "127.0.0.1")
APP_PORT = int(os.getenv("WEBHOOK_INSPECTOR_PORT", "18355"))
PAGE_SIZE = 50
@@ -23,6 +24,12 @@ def get_conn():
return conn
def get_state_conn():
conn = sqlite3.connect(STATE_DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def html_page(title: str, body: str) -> HTMLResponse:
nav = """
<nav>
@@ -30,6 +37,7 @@ def html_page(title: str, body: str) -> HTMLResponse:
<a href='/events'>Events</a>
<a href='/objects'>Objects</a>
<a href='/form-responses'>Form responses</a>
<a href='/generated-materials'>Generated materials</a>
</nav>
"""
css = """
@@ -114,7 +122,7 @@ def link_with_params(path, **params):
@app.get("/health")
def health():
return {"ok": True, "db_path": DB_PATH}
return {"ok": True, "db_path": DB_PATH, "state_db_path": STATE_DB_PATH}
@app.get("/", response_class=HTMLResponse)
@@ -129,6 +137,17 @@ def dashboard():
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
counts["generated_job_materials"] = state_count
cards = "".join(
f"<div class='card'><div class='muted'>{escape(name)}</div><div class='big'>{count}</div></div>"
for name, count in counts.items()
@@ -138,9 +157,11 @@ def dashboard():
<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>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>
</div>
<div class='section'>
@@ -149,6 +170,7 @@ def dashboard():
<li><a href='/events'>Browse webhook events</a></li>
<li><a href='/objects'>Browse object webhooks</a></li>
<li><a href='/form-responses'>Browse form responses</a></li>
<li><a href='/generated-materials'>Browse generated job-material state</a></li>
</ul>
</div>
"""
@@ -364,6 +386,73 @@ def list_form_responses(page: int = Query(1, ge=1)):
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>{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 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='7'>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 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: