From f7271e22ae979548e482b5f0037ad4618e40472a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 17 Mar 2026 00:39:01 -0400 Subject: [PATCH] feat: include supporting data in all export formats Query supporting data in the export endpoint and pass to markdown, text, HTML, and PSA export generators. Each format renders text snippets and screenshot placeholders in its native style. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/endpoints/sessions.py | 25 +++++++++-- backend/app/services/export_service.py | 59 ++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/backend/app/api/endpoints/sessions.py b/backend/app/api/endpoints/sessions.py index 47a238a5..ea4375f1 100644 --- a/backend/app/api/endpoints/sessions.py +++ b/backend/app/api/endpoints/sessions.py @@ -407,18 +407,35 @@ async def export_session( headers={"Content-Disposition": f'attachment; filename="session-export-{session_id}.pdf"'}, ) + # Query supporting data for non-PDF formats + from app.models.supporting_data import SessionSupportingData + sd_result = await db.execute( + select(SessionSupportingData) + .where(SessionSupportingData.session_id == session_id) + .order_by(SessionSupportingData.sort_order) + ) + supporting_data_items = [ + { + "label": sd.label, + "data_type": sd.data_type, + "content": sd.content, + "content_type": sd.content_type, + } + for sd in sd_result.scalars().all() + ] + # Generate export based on format if export_options.format == "markdown": - content = generate_markdown_export(session, export_options) + content = generate_markdown_export(session, export_options, supporting_data=supporting_data_items) media_type = "text/markdown" elif export_options.format == "html": - content = generate_html_export(session, export_options) + content = generate_html_export(session, export_options, supporting_data=supporting_data_items) media_type = "text/html" elif export_options.format == "psa": - content = generate_psa_export(session, export_options) + content = generate_psa_export(session, export_options, supporting_data=supporting_data_items) media_type = "text/plain" else: # text - content = generate_text_export(session, export_options) + content = generate_text_export(session, export_options, supporting_data=supporting_data_items) media_type = "text/plain" # Resolve variables in export output diff --git a/backend/app/services/export_service.py b/backend/app/services/export_service.py index 15e1426b..7d3d8091 100644 --- a/backend/app/services/export_service.py +++ b/backend/app/services/export_service.py @@ -171,7 +171,7 @@ def _escape_markdown_table(value: str) -> str: return value.replace("|", "\\|").replace("\n", " ") -def generate_markdown_export(session: Session, options: SessionExport) -> str: +def generate_markdown_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str: """Generate markdown export.""" if _is_procedural_session(session): return _generate_procedural_markdown(session, options) @@ -261,6 +261,22 @@ def generate_markdown_export(session: Session, options: SessionExport) -> str: lines.append(f"*{decision['timestamp']}*") lines.append("") + # Supporting Data + if supporting_data: + lines.append("---") + lines.append("") + lines.append("## Supporting Data") + lines.append("") + for sd in supporting_data: + lines.append(f"### {sd['label']}") + if sd["data_type"] == "text_snippet": + lines.append("```") + lines.append(sd["content"]) + lines.append("```") + else: + lines.append(f"[Screenshot: {sd['label']}]") + lines.append("") + # Resolution / Outcome Notes _raw_notes = getattr(session, 'outcome_notes', None) outcome_notes = _raw_notes if isinstance(_raw_notes, str) else '' @@ -286,7 +302,7 @@ def generate_markdown_export(session: Session, options: SessionExport) -> str: return "\n".join(lines) -def generate_text_export(session: Session, options: SessionExport) -> str: +def generate_text_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str: """Generate plain text export.""" if _is_procedural_session(session): return _generate_procedural_text(session, options) @@ -361,6 +377,19 @@ def generate_text_export(session: Session, options: SessionExport) -> str: if duration_seconds is not None: lines.append(f" Duration: {_format_step_duration(duration_seconds)}") + # Supporting Data + if supporting_data: + lines.append("") + lines.append("SUPPORTING DATA") + lines.append("-" * 20) + for sd in supporting_data: + lines.append(f"\n {sd['label']}:") + if sd["data_type"] == "text_snippet": + for content_line in sd["content"].splitlines(): + lines.append(f" {content_line}") + else: + lines.append(f" [Screenshot: {sd['label']}]") + # Resolution _raw_notes = getattr(session, 'outcome_notes', None) outcome_notes = _raw_notes if isinstance(_raw_notes, str) else '' @@ -382,7 +411,7 @@ def generate_text_export(session: Session, options: SessionExport) -> str: return "\n".join(lines) -def generate_html_export(session: Session, options: SessionExport) -> str: +def generate_html_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str: """Generate HTML export.""" if _is_procedural_session(session): return _generate_procedural_html(session, options) @@ -472,6 +501,15 @@ def generate_html_export(session: Session, options: SessionExport) -> str: html_parts.append(f'

{html.escape(str(decision["timestamp"]))}

') html_parts.append('') + # Supporting Data + if supporting_data: + html_parts.append('

Supporting Data

') + for sd in supporting_data: + if sd["data_type"] == "text_snippet": + html_parts.append(f'

{html.escape(sd["label"])}

{html.escape(sd["content"])}
') + else: + html_parts.append(f'

{html.escape(sd["label"])}

{html.escape(sd[
') + # Resolution _raw_notes = getattr(session, 'outcome_notes', None) outcome_notes = _raw_notes if isinstance(_raw_notes, str) else '' @@ -490,7 +528,7 @@ def generate_html_export(session: Session, options: SessionExport) -> str: return "\n".join(html_parts) -def generate_psa_export(session: Session, options: SessionExport) -> str: +def generate_psa_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str: """Generate PSA/ticket note export optimized for ConnectWise and similar PSA tools.""" if _is_procedural_session(session): return _generate_procedural_psa(session, options) @@ -561,6 +599,19 @@ def generate_psa_export(session: Session, options: SessionExport) -> str: lines.append("No steps recorded.") lines.append("") + # Supporting Data + if supporting_data: + lines.append("--- SUPPORTING DATA ---") + for sd in supporting_data: + if sd["data_type"] == "text_snippet": + lines.append(f"## {sd['label']}") + lines.append("```") + lines.append(sd["content"]) + lines.append("```") + else: + lines.append(f"[Screenshot: {sd['label']}]") + lines.append("") + # Resolution — only for completed sessions if session.completed_at: lines.append("--- RESOLUTION ---")