"""Daily Brief HTML rendering. Pure-stdlib module — has no dependencies on the rest of the app, so it can be imported by preview scripts, tests, and the scheduler alike. The shape of the items list matches a SELECT * from the `daily_brief` table. """ from __future__ import annotations import json def build_brief_html(items: list[dict]) -> str: """Render the Daily Brief email body from `daily_brief` rows. Sections rendered (in order, only if non-empty): 1. Ambiguous Routings — `reason == "ambiguous"` 2. MTRs Missing Heat Code — `reason == "mtr_no_heat_code"` 3. Processing Errors — `reason == "error"` If all sections are empty, returns a short "nothing to report" body. """ ambiguous = [i for i in items if i["reason"] == "ambiguous"] no_heat = [i for i in items if i["reason"] == "mtr_no_heat_code"] unmatched = [i for i in items if i["reason"] == "client_unmatched"] errors = [i for i in items if i["reason"] == "error"] sections: list[str] = [] # One-line summary at top so ops doesn't need to count rows. summary_bits = [] if ambiguous: summary_bits.append(f"{len(ambiguous)} ambiguous routing(s)") if no_heat: summary_bits.append(f"{len(no_heat)} MTR(s) without a heat code") if unmatched: summary_bits.append(f"{len(unmatched)} unmatched sender domain(s)") if errors: summary_bits.append(f"{len(errors)} processing error(s)") if summary_bits: sections.append( f"

Today's automation summary: {'; '.join(summary_bits)}.

" ) if ambiguous: rows = "" for item in ambiguous: scores: dict = ( json.loads(item["confidence_scores"]) if item.get("confidence_scores") else {} ) score_cols = "".join( f"{doc_type}: {score:.2%}" for doc_type, score in sorted(scores.items()) ) rows += ( f"" f"{item.get('sender_email', '')}" f"{item.get('subject', '')}" f"{item.get('attachment_filename', '')}" f"{score_cols}" f"" ) first_scores = ( json.loads(ambiguous[0]["confidence_scores"]) if ambiguous[0].get("confidence_scores") else {} ) score_headers = "".join( f"{doc_type}" for doc_type in sorted(first_scores.keys()) ) sections.append( "

Ambiguous Routings

" "

These attachments scored above the confidence threshold for " "more than one document type and were not auto-routed. Review and " "file manually.

" "" f"{score_headers}" f"{rows}" "
SenderSubjectAttachment
" ) if no_heat: rows = "" for item in no_heat: rows += ( f"" f"{item.get('sender_email', '')}" f"{item.get('subject', '')}" f"{item.get('attachment_filename', '')}" f"" ) sections.append( "

MTRs Missing Heat Code

" "

These MTRs were uploaded to MTR/_no_heat_code/ " "because no heat code could be extracted from the document. " "Please review the file in Dropbox, identify the heat code(s), " "and move/copy under the correct MTR/<heat_code>/ " "folder.

" "" "" f"{rows}" "
SenderSubjectAttachment
" ) if unmatched: rows = "" for item in unmatched: rows += ( f"" f"{item.get('sender_email', '')}" f"{item.get('subject', '')}" f"{item.get('attachment_filename', '')}" f"" ) sections.append( "

Unmatched Sender Domains

" "

These POs/Invoices were uploaded to " "POs/_unmatched/ or Invoices/_unmatched/ " "because the sender's email domain isn't in the client mapping. " "Move the file under the right client folder in Dropbox, then " "add the domain → client mapping to " "scripts/seed_client_domains.json and re-run the " "seeder so future emails route correctly.

" "" "" f"{rows}" "
SenderSubjectAttachment
" ) if errors: rows = "" for item in errors: rows += ( f"" f"{item.get('sender_email', '')}" f"{item.get('subject', '')}" f"{item.get('attachment_filename', '')}" f"{item.get('error_message', '')}" f"" ) sections.append( "

Processing Errors

" "

These attachments raised an exception during processing. " "Stack traces are in the processor log files.

" "" "" f"{rows}" "
SenderSubjectAttachmentError
" ) if not sections: return ( "" "

No items to report today — all attachments routed cleanly.

" "" ) return f"{''.join(sections)}"