From acf1ff2547a5f43991fce9ada1c295953aa2df0b Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Thu, 28 May 2026 17:04:55 -0500 Subject: [PATCH 1/5] feat: add Download JSON-LD action to report viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the new ``LedgerClient.getReportBundleDownloadUrl`` SDK method (0.3.35) into the saved-report viewer at /reports/[id]. The button sits in the page-header actions row next to Share, scoped to ``generationStatus === 'published'`` Reports — pre-publish drafts have no stamped bundle to download. Click flow: 1. SDK call fetches a short-lived presigned URL from the backend (300s lifetime). 2. ``window.location.href = downloadUrl`` triggers the browser download — the backend sets Content-Disposition: attachment with a versioned filename (``{report_id}-v{n}.jsonld``), so navigating to the URL writes the file to disk rather than rendering it inline. Error feedback surfaces in a dismissible Alert above the status banner, carrying the server-returned detail string (e.g. "Report has no stamped bundle — regenerate to produce one", which is the expected failure for Reports published before the serialization feature shipped). In-flight state disables the button and swaps the icon for a spinner — the SDK round-trip is the only thing the user waits for; the download itself starts as soon as the browser hits the presigned URL. --- src/app/(app)/reports/[id]/content.tsx | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/app/(app)/reports/[id]/content.tsx b/src/app/(app)/reports/[id]/content.tsx index 61e90b4..d836ed0 100644 --- a/src/app/(app)/reports/[id]/content.tsx +++ b/src/app/(app)/reports/[id]/content.tsx @@ -21,6 +21,7 @@ import type { FC } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { HiChevronLeft, + HiDocumentDownload, HiDocumentReport, HiExclamationCircle, HiShare, @@ -86,6 +87,12 @@ const ReportViewerContent: FC = function () { const [isSharing, setIsSharing] = useState(false) const [shareResult, setShareResult] = useState(null) + // Bundle-download state — feedback-only; the actual download happens + // by navigating to a presigned URL, so the spinner is just the + // round-trip to fetch the URL. + const [isDownloadingBundle, setIsDownloadingBundle] = useState(false) + const [downloadError, setDownloadError] = useState(null) + const loadPublishLists = useCallback(async () => { if (!graphId) return try { @@ -99,6 +106,36 @@ const ReportViewerContent: FC = function () { } }, [graphId]) + // Trigger a browser download of the Report's stamped JSON-LD bundle. + // The backend returns a presigned URL with Content-Disposition: + // attachment set, so navigating to it forces the file to download + // rather than rendering inline. ``window.location.href`` is used over + // a hidden ```` so we don't have to manage DOM lifetime + // and the browser handles cross-origin attachment headers cleanly. + const handleDownloadBundle = useCallback(async () => { + if (!graphId || !reportId) return + try { + setIsDownloadingBundle(true) + setDownloadError(null) + const resp = await clients.reports.getReportBundleDownloadUrl( + graphId, + reportId + ) + window.location.href = resp.downloadUrl + } catch (err) { + console.error('Bundle download failed:', err) + // 404 message from the backend ("no stamped bundle — regenerate to + // produce one") is the most actionable failure mode for users + // viewing a pre-feature Report, so surface the server detail when + // available rather than a generic string. + const message = + err instanceof Error ? err.message : 'Failed to start download.' + setDownloadError(message) + } finally { + setIsDownloadingBundle(false) + } + }, [graphId, reportId]) + const handleShare = useCallback(async () => { if (!graphId || !reportId || !selectedListId) return @@ -246,6 +283,22 @@ const ReportViewerContent: FC = function () { gradient="from-orange-500 to-red-600" actions={ <> + {pkg.generationStatus === 'published' && ( + + )} {pkg.generationStatus === 'published' && !pkg.sourceGraphId && ( + + JSON-LD bundle + + + XBRL 2.1 package + + )} {pkg.generationStatus === 'published' && !pkg.sourceGraphId && (