@@ -585,32 +585,55 @@ async function shouldRewrite(targetDirHandle) {
585585 return { rewrite : false , reason : "existing-preview-without-capture-timeout" } ;
586586}
587587
588- function downloadWorkspacePreview ( svgContent , outputPath ) {
589- const BlobCtor = window . Blob ;
590- const urlApi = window . URL || window . webkitURL ;
591- if ( typeof BlobCtor !== "function" || ! urlApi ?. createObjectURL ) {
592- throw new Error ( "Browser download APIs are unavailable for workspace launch output." ) ;
593- }
594- const blob = new BlobCtor ( [ svgContent ] , { type : "image/svg+xml" } ) ;
595- const url = urlApi . createObjectURL ( blob ) ;
596- const link = window . document . createElement ( "a" ) ;
597- link . href = url ;
598- link . download = OUTPUT_NAME ;
599- link . rel = "noopener" ;
600- link . dataset . workspaceOutputPath = outputPath ;
601- window . document . body . append ( link ) ;
602- link . click ( ) ;
603- link . remove ( ) ;
604- window . setTimeout ( ( ) => {
605- urlApi . revokeObjectURL ( url ) ;
606- } , 0 ) ;
588+ function previewWriteError ( message ) {
589+ const error = new Error ( message ) ;
590+ error . previewWriteFailed = true ;
591+ return error ;
592+ }
593+
594+ function isPreviewWriteError ( error ) {
595+ return Boolean ( error ?. previewWriteFailed ) ;
596+ }
597+
598+ function validateWorkspacePreviewWritePath ( entry ) {
599+ if ( ! isWorkspaceManagerLaunch ( ) || ! workspaceRepoRootHydrated || ! workspacePreviewFileValid ) {
600+ throw previewWriteError ( "Workspace preview write path is unavailable because launch hydration is incomplete." ) ;
601+ }
602+ const targetPath = normalizeWorkspacePath ( getWorkspacePreviewTargetDisplayPath ( ) ) ;
603+ if ( ! targetPath ) {
604+ throw previewWriteError ( "Workspace preview write path is empty." ) ;
605+ }
606+ const targetParts = targetPath . split ( "/" ) ;
607+ if ( targetParts . includes ( ".." ) || targetPath . startsWith ( "." ) ) {
608+ throw previewWriteError ( `Workspace preview write path is invalid: ${ targetPath } .` ) ;
609+ }
610+ const expectedPrefix = `games/${ entry . name } /` ;
611+ if ( entry . targetType !== "games" || entry . name !== workspacePreviewGameId || ! targetPath . startsWith ( expectedPrefix ) ) {
612+ throw previewWriteError ( `Workspace preview write path ${ targetPath } does not match the hydrated ${ workspacePreviewGameId } game context.` ) ;
613+ }
614+ return targetPath ;
615+ }
616+
617+ async function writeWorkspacePreview ( svgContent , entry ) {
618+ const targetPath = validateWorkspacePreviewWritePath ( entry ) ;
619+ const targetUrl = new URL ( `/${ targetPath } ` , window . location . href ) ;
620+ const response = await fetch ( targetUrl . href , {
621+ method : "PUT" ,
622+ headers : {
623+ "Content-Type" : "image/svg+xml; charset=utf-8"
624+ } ,
625+ body : svgContent
626+ } ) ;
627+ if ( ! response . ok ) {
628+ const details = await response . text ( ) . catch ( ( ) => "" ) ;
629+ throw previewWriteError ( `${ targetPath } returned ${ response . status } ${ details ? `: ${ details } ` : "" } .` ) ;
630+ }
631+ logger . log ( `Direct preview write target: ${ targetPath } ` ) ;
607632}
608633
609634async function writePreview ( targetDirHandle , svgContent , entry ) {
610635 if ( ! targetDirHandle && isWorkspaceManagerLaunch ( ) && workspaceRepoRootHydrated ) {
611- const outputPath = getFullOutputPath ( entry ) ;
612- downloadWorkspacePreview ( svgContent , outputPath ) ;
613- logger . log ( `DL ${ outputPath } ` ) ;
636+ await writeWorkspacePreview ( svgContent , entry ) ;
614637 return ;
615638 }
616639
@@ -678,8 +701,26 @@ async function processOne(entry, baseUrl, waitMs) {
678701 logger . log ( "" ) ;
679702 return { id : label , status : "written" , reason : decision . reason } ;
680703 } catch ( error ) {
704+ if ( isPreviewWriteError ( error ) ) {
705+ logger . log ( `FAIL Direct preview write failed: ${ error . message } ` ) ;
706+ logger . log ( "" ) ;
707+ return { id : label , status : "failed" , reason : error . message } ;
708+ }
681709 const fallback = capture . buildFallbackSvg ( `${ CAPTURE_TIMEOUT_MARKER } : ${ error . message } ` ) ;
682- await writePreview ( targetDirHandle , fallback , entry ) ;
710+ try {
711+ await writePreview ( targetDirHandle , fallback , entry ) ;
712+ } catch ( writeError ) {
713+ const reason = isPreviewWriteError ( writeError )
714+ ? writeError . message
715+ : `${ error . message } ; fallback write failed: ${ writeError . message } ` ;
716+ if ( isPreviewWriteError ( writeError ) ) {
717+ logger . log ( `FAIL Direct preview write failed: ${ writeError . message } ` ) ;
718+ } else {
719+ logger . log ( `FAIL ${ label } (${ reason } )` ) ;
720+ }
721+ logger . log ( "" ) ;
722+ return { id : label , status : "failed" , reason } ;
723+ }
683724 ui . setLastGeneratedImage ( fallback , label ) ;
684725 logger . log ( `FAIL ${ label } (${ error . message } )` ) ;
685726 logger . log ( "" ) ;
@@ -840,7 +881,7 @@ class PreviewGeneratorV2App {
840881 logger . log ( `Capture mode: ${ ui . getCaptureModeLabel ( ) } ` ) ;
841882 logger . log ( `Force rewrite: ${ ui . renderControls . isForceRewrite ( ) } ` ) ;
842883 if ( ! repoDirHandle && isWorkspaceManagerLaunch ( ) ) {
843- logger . log ( " Workspace launch repo root was hydrated from context; output will download because browsers cannot write to a repo path without a directory handle." ) ;
884+ logger . log ( ` Workspace launch direct preview write target: ${ getWorkspacePreviewTargetDisplayPath ( ) || "unavailable" } .` ) ;
844885 }
845886 logger . log ( "" ) ;
846887
0 commit comments