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.
This commit is contained in:
Anish Sarkar 2026-02-12 03:41:29 +05:30
parent adeef35443
commit a668219240
2 changed files with 27 additions and 23 deletions

View file

@ -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. No create or update endpoints here reports are generated inline by the
Reports are generated inline by the agent tool and stored as Markdown. agent tool during chat and stored as Markdown in the database.
Export to PDF/DOCX is on-demand via pypandoc. Export to PDF/DOCX is on-demand via pypandoc.
Authorization: lightweight search-space membership checks (no granular RBAC) 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 asyncio
import io import io
import logging import logging
import os
import tempfile
from enum import Enum from enum import Enum
import pypandoc import pypandoc
@ -205,26 +207,32 @@ async def export_report(
) )
# Convert Markdown to the requested format via pypandoc. # Convert Markdown to the requested format via pypandoc.
# pypandoc spawns a pandoc subprocess (blocking), so we run it in a # pypandoc spawns a pandoc subprocess (blocking), so we run the
# thread executor to avoid blocking the async event loop. # entire convert → read → cleanup pipeline in a thread executor
# to avoid blocking the async event loop on any file I/O.
extra_args = ["--standalone"] extra_args = ["--standalone"]
if format == ExportFormat.PDF: if format == ExportFormat.PDF:
extra_args.append("--pdf-engine=weasyprint") extra_args.append("--pdf-engine=weasyprint")
loop = asyncio.get_running_loop() def _convert_and_read() -> bytes:
output = await loop.run_in_executor( """Run all blocking I/O (tempfile, pandoc, file read, cleanup) in a thread."""
None, # default thread-pool fd, tmp_path = tempfile.mkstemp(suffix=f".{format.value}")
lambda: pypandoc.convert_text( os.close(fd)
report.content, try:
format.value, pypandoc.convert_text(
format="md", report.content,
extra_args=extra_args, 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 loop = asyncio.get_running_loop()
if isinstance(output, str): output = await loop.run_in_executor(None, _convert_and_read)
output = output.encode("utf-8")
# Sanitize filename # Sanitize filename
safe_title = ( safe_title = (

View file

@ -3,7 +3,6 @@
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { import {
ChevronDownIcon, ChevronDownIcon,
FileTextIcon,
XIcon, XIcon,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
@ -257,12 +256,9 @@ function ReportPanelContent({
if (error || !reportContent) { if (error || !reportContent) {
return ( return (
<div className="flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center"> <div className="flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-muted">
<FileTextIcon className="size-6 text-muted-foreground" />
</div>
<div> <div>
<p className="font-medium text-foreground">Failed to load report</p> <p className="font-medium text-foreground">Failed to load report</p>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-red-500 mt-1">
{error || "An unknown error occurred"} {error || "An unknown error occurred"}
</p> </p>
</div> </div>