From a668219240e79dad718be1b17c6bbb7b036e0599 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:41:29 +0530 Subject: [PATCH] refactor: update report routes and improve export handling - Revised report routes to clarify functionality for read, export, and delete operations. - Enhanced the export process to run all blocking I/O in a thread executor, improving async performance. - Updated error handling in the report panel to provide clearer feedback on loading failures. --- .../app/routes/reports_routes.py | 44 +++++++++++-------- .../components/report-panel/report-panel.tsx | 6 +-- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/surfsense_backend/app/routes/reports_routes.py b/surfsense_backend/app/routes/reports_routes.py index 49a74b658..9a250ce29 100644 --- a/surfsense_backend/app/routes/reports_routes.py +++ b/surfsense_backend/app/routes/reports_routes.py @@ -1,8 +1,8 @@ """ -Report routes for CRUD operations and export (PDF/DOCX). +Report routes for read, export (PDF/DOCX), and delete operations. -These routes support the report generation feature in new-chat. -Reports are generated inline by the agent tool and stored as Markdown. +No create or update endpoints here — reports are generated inline by the +agent tool during chat and stored as Markdown in the database. Export to PDF/DOCX is on-demand via pypandoc. Authorization: lightweight search-space membership checks (no granular RBAC) @@ -12,6 +12,8 @@ since reports are chat-generated artifacts, not standalone managed resources. import asyncio import io import logging +import os +import tempfile from enum import Enum import pypandoc @@ -205,26 +207,32 @@ async def export_report( ) # Convert Markdown to the requested format via pypandoc. - # pypandoc spawns a pandoc subprocess (blocking), so we run it in a - # thread executor to avoid blocking the async event loop. + # pypandoc spawns a pandoc subprocess (blocking), so we run the + # entire convert → read → cleanup pipeline in a thread executor + # to avoid blocking the async event loop on any file I/O. extra_args = ["--standalone"] if format == ExportFormat.PDF: extra_args.append("--pdf-engine=weasyprint") - loop = asyncio.get_running_loop() - output = await loop.run_in_executor( - None, # default thread-pool - lambda: pypandoc.convert_text( - report.content, - format.value, - format="md", - extra_args=extra_args, - ), - ) + def _convert_and_read() -> bytes: + """Run all blocking I/O (tempfile, pandoc, file read, cleanup) in a thread.""" + fd, tmp_path = tempfile.mkstemp(suffix=f".{format.value}") + os.close(fd) + try: + pypandoc.convert_text( + report.content, + format.value, + format="md", + extra_args=extra_args, + outputfile=tmp_path, + ) + with open(tmp_path, "rb") as f: + return f.read() + finally: + os.unlink(tmp_path) - # pypandoc returns bytes for binary formats (pdf, docx), str for text formats - if isinstance(output, str): - output = output.encode("utf-8") + loop = asyncio.get_running_loop() + output = await loop.run_in_executor(None, _convert_and_read) # Sanitize filename safe_title = ( diff --git a/surfsense_web/components/report-panel/report-panel.tsx b/surfsense_web/components/report-panel/report-panel.tsx index 1a13e1e00..5b91b4e69 100644 --- a/surfsense_web/components/report-panel/report-panel.tsx +++ b/surfsense_web/components/report-panel/report-panel.tsx @@ -3,7 +3,6 @@ import { useAtomValue, useSetAtom } from "jotai"; import { ChevronDownIcon, - FileTextIcon, XIcon, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -257,12 +256,9 @@ function ReportPanelContent({ if (error || !reportContent) { return (
Failed to load report
-+
{error || "An unknown error occurred"}