Files
fusion/packages/dashboard/app/utils/report-capture.ts
gsxdsm a9c7a6bcc0 FN-8371: store report screenshot artifacts
Store opted-in report screenshots as validated local artifacts rather than GitHub attachments.

- Add multipart report attachment upload with PNG/JPEG validation and provenance metadata.
- Carry validated artifact references through report drafts and filing flows without binary egress.
- Update report UI, tests, documentation, and release metadata.

Files changed:
 .changeset/report-screenshot-artifacts.md          |   7 +
 docs/dashboard-guide.md                            |   2 +-
 packages/core/src/index.gate.ts                    |   2 +-
 packages/core/src/index.ts                         |   2 +-
 packages/core/src/types.ts                         |   7 +
 packages/dashboard/app/api/report.ts               |  14 +-
 packages/dashboard/app/components/ReportModal.css  |   3 +-
 packages/dashboard/app/components/ReportModal.tsx  |  41 ++---
 .../app/components/__tests__/ReportModal.test.tsx  |  58 +++++--
 packages/dashboard/app/utils/report-capture.ts     |  47 ++----
 .../src/__tests__/report-pipeline.test.ts          |  94 ------------
 .../dashboard/src/__tests__/report-routes.test.ts  | 123 +++++++++++----
 packages/dashboard/src/report-pipeline.ts          | 128 +++-------------
 .../dashboard/src/routes/register-report-routes.ts | 169 +++++++--------------
 14 files changed, 279 insertions(+), 418 deletions(-)

Fusion-Task-Id: FN-8371

Fusion-Task-Lineage: 48d0e94d-602f-43e2-a63f-cb497221ac29

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 16:44:40 -07:00

23 lines
1.3 KiB
TypeScript

const MAX_ACTIVITY = 20;
const activity: string[] = [];
/**
* FNXC:ReportPipeline 2026-07-19-10:00:
* Capture returns a local PNG blob solely for immediate artifact upload. The
* caller must not serialize it into a report payload or display it as a draft.
*/
export async function captureScreenshot(): Promise<Blob | undefined> {
if (!navigator.mediaDevices?.getDisplayMedia) return undefined;
let stream: MediaStream | undefined;
try {
stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
const video = document.createElement("video"); video.srcObject = stream; await video.play();
const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight;
canvas.getContext("2d")?.drawImage(video, 0, 0);
return await new Promise<Blob | undefined>((resolve) => canvas.toBlob((blob) => resolve(blob ?? undefined), "image/png"));
} catch { return undefined; } finally { stream?.getTracks().forEach((track) => track.stop()); }
}
export function recordActivity(label: string): void { activity.push(label.slice(0, 80)); while (activity.length > MAX_ACTIVITY) activity.shift(); }
export function getRecentActivity(): string[] { return [...activity]; }
export function clearReportActivityForTests(): void { activity.length = 0; }