diff --git a/crates/desktop/artcraft/src/core/api_adapters/models/image/tauri_image_model_to_enums_model.rs b/crates/desktop/artcraft/src/core/api_adapters/models/image/tauri_image_model_to_enums_model.rs
--- a/crates/desktop/artcraft/src/core/api_adapters/models/image/tauri_image_model_to_enums_model.rs
+++ b/crates/desktop/artcraft/src/core/api_adapters/models/image/tauri_image_model_to_enums_model.rs
@@ -8,7 +8,7 @@ pub fn tauri_image_model_to_enums_model(model: TauriImageModel) -> Option<Common
   match model {
     TauriImageModel::Flux1Dev => Some(CommonImageModel::Flux1Dev),
     TauriImageModel::Flux1Schnell => Some(CommonImageModel::Flux1Schnell),
-    TauriImageModel::FluxPro1 => Some(CommonImageModel::FluxPro11), // TODO: Might be a slight mismatch
+    TauriImageModel::FluxPro1 => None, // Dedicated inpaint endpoint; Flux Pro 1.1 cannot inpaint.
     TauriImageModel::FluxPro11 => Some(CommonImageModel::FluxPro11),
     TauriImageModel::FluxPro11Ultra => Some(CommonImageModel::FluxPro11Ultra),
     TauriImageModel::GptImage1 => Some(CommonImageModel::GptImage1),
@@ -36,3 +36,14 @@ pub fn tauri_image_model_to_enums_model(model: TauriImageModel) -> Option<Common
     TauriImageModel::FluxDevJuggernaut => None,
   }
 }
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn flux_inpaint_must_not_be_sent_as_flux_pro_11() {
+    assert!(tauri_image_model_to_enums_model(TauriImageModel::FluxPro1).is_none());
+    assert!(matches!(tauri_image_model_to_enums_model(TauriImageModel::FluxPro11), Some(CommonImageModel::FluxPro11)));
+  }
+}
diff --git a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft.rs b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft.rs
--- a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft.rs
+++ b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft.rs
@@ -59,5 +59,18 @@ fn is_legacy_only_model(model: TauriImageModel) -> bool {
       | TauriImageModel::Midjourney
       | TauriImageModel::FluxProKontextMax
       | TauriImageModel::FluxDevJuggernaut
+      | TauriImageModel::FluxPro1
   )
 }
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn flux_inpaint_uses_legacy_but_text_to_image_uses_omni() {
+    assert!(is_legacy_only_model(TauriImageModel::FluxPro1));
+    assert!(!is_legacy_only_model(TauriImageModel::FluxPro11));
+    assert!(!is_legacy_only_model(TauriImageModel::FluxPro11Ultra));
+  }
+}
diff --git a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft_via_legacy.rs b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft_via_legacy.rs
--- a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft_via_legacy.rs
+++ b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/handle_artcraft_via_legacy.rs
@@ -1,3 +1,4 @@
+use super::legacy::artcraft_flux_pro_1_inpaint::handle_flux_pro_1_inpaint;
 use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
 
 use crate::core::commands::enqueue::generate_error::GenerateError;
@@ -23,6 +24,9 @@ pub async fn handle_artcraft_via_legacy(
   let model = request.model.ok_or(GenerateError::no_model_specified())?;
 
   match model {
+    TauriImageModel::FluxPro1 => {
+      handle_flux_pro_1_inpaint(request, semantic_media_files, creds, app_env_configs).await
+    }
     TauriImageModel::FluxDevJuggernaut => {
       handle_flux_dev_juggernaut_inpaint(request, semantic_media_files, creds, app_env_configs).await
     }
diff --git a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/artcraft_flux_pro_1_inpaint.rs b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/artcraft_flux_pro_1_inpaint.rs
new file mode 100644
--- /dev/null
+++ b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/artcraft_flux_pro_1_inpaint.rs
@@ -0,0 +1,124 @@
+use log::{error, info};
+
+use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
+use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
+use artcraft_client::endpoints::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
+use enums::common::generation_provider::GenerationProvider;
+use enums::tauri::tasks::task_type::TaskType;
+use uuid_utils::uuid::generate_random_uuid;
+
+use tokens::tokens::media_files::MediaFileToken;
+
+use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
+use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
+use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
+use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
+use crate::core::events::generation_events::common::GenerationModel;
+use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
+
+/// Handle FluxPro1 inpainting via the legacy dedicated endpoint.
+///
+/// Requires a source image (from scene, canvas, or image_media_tokens) and an inpainting mask.
+pub async fn handle_flux_pro_1_inpaint(request: &TauriGenerateImageRequest, semantic_media_files: &SemanticMediaFiles, creds: &StorytellerCredentialSet, app_env_configs: &AppEnvConfigs) -> Result<TaskEnqueueSuccess, GenerateError> {
+  let inpaint_request = build_inpaint_request(request, semantic_media_files)?;
+
+  info!("Calling FluxPro1 inpaint...");
+
+  let response = flux_pro_1_inpaint_image(&app_env_configs.storyteller_host, Some(creds), inpaint_request).await.map_err(|err| {
+    error!("FluxPro1 inpaint failed: {:?}", err);
+    GenerateError::from(err)
+  })?;
+
+  info!("FluxPro1 inpaint succeeded: job_token={}", response.inference_job_token);
+
+  Ok(TaskEnqueueSuccess { task_type: TaskType::ImageInpaintEdit, model: Some(GenerationModel::FluxPro1), provider: GenerationProvider::Artcraft, provider_job_id: Some(response.inference_job_token.to_string()), maybe_queue_status_url: None, maybe_prompt_token: None, maybe_queue_response_url: None })
+}
+
+fn build_inpaint_request(request: &TauriGenerateImageRequest, semantic_media_files: &SemanticMediaFiles) -> Result<FluxPro1InpaintImageRequest, GenerateError> {
+  let image_media_token = get_first_image_token(request, semantic_media_files).ok_or(GenerateError::required_source_image_not_provided())?;
+
+  let mask_media_token = semantic_media_files.inpainting_mask_image_media_token.clone().ok_or(GenerateError::required_source_image_mask_not_provided())?;
+
+  let num_images = match request.batch_size {
+    None => None,
+    Some(1) => Some(FluxPro1InpaintImageNumImages::One),
+    Some(2) => Some(FluxPro1InpaintImageNumImages::Two),
+    Some(3) => Some(FluxPro1InpaintImageNumImages::Three),
+    Some(4) => Some(FluxPro1InpaintImageNumImages::Four),
+    Some(other) => {
+      return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { min: 1, max: 4, requested: other }));
+    },
+  };
+
+  let uuid_idempotency_token = generate_random_uuid();
+
+  Ok(FluxPro1InpaintImageRequest { uuid_idempotency_token, prompt: request.prompt.clone(), image_media_token, mask_media_token, num_images })
+}
+
+fn get_first_image_token(request: &TauriGenerateImageRequest, semantic_media_files: &SemanticMediaFiles) -> Option<MediaFileToken> {
+  if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
+    return Some(scene_token.clone());
+  }
+  if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
+    return Some(canvas_token.clone());
+  }
+  if let Some(media_tokens) = &request.image_media_tokens {
+    if let Some(token) = media_tokens.first() {
+      return Some(token.clone());
+    }
+  }
+  None
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use serde_json::json;
+
+  #[test]
+  fn builds_dedicated_inpaint_payload_with_source_mask_prompt_and_count() {
+    let request = request(json!({"image_media_tokens": ["mf_source"], "prompt": "a vase", "batch_size": 3}));
+    let built = build_inpaint_request(&request, &media(None, None, Some("mf_mask"))).unwrap();
+    assert_eq!(built.image_media_token.as_str(), "mf_source");
+    assert_eq!(built.mask_media_token.as_str(), "mf_mask");
+    assert_eq!(built.prompt.as_deref(), Some("a vase"));
+    assert!(matches!(built.num_images, Some(FluxPro1InpaintImageNumImages::Three)));
+    assert!(!built.uuid_idempotency_token.is_empty());
+  }
+
+  #[test]
+  fn semantic_sources_take_precedence_over_references() {
+    let request = request(json!({"image_media_tokens": ["mf_reference"]}));
+    for (scene, canvas, expected) in [(Some("mf_scene"), Some("mf_canvas"), "mf_scene"), (None, Some("mf_canvas"), "mf_canvas")] {
+      let built = build_inpaint_request(&request, &media(scene, canvas, Some("mf_mask"))).unwrap();
+      assert_eq!(built.image_media_token.as_str(), expected);
+    }
+  }
+
+  #[test]
+  fn rejects_missing_source_and_mask_before_submitting() {
+    assert!(build_inpaint_request(&request(json!({})), &media(None, None, Some("mf_mask"))).is_err());
+    assert!(build_inpaint_request(&request(json!({"image_media_tokens": []})), &media(None, None, Some("mf_mask"))).is_err());
+    assert!(build_inpaint_request(&request(json!({"image_media_tokens": ["mf_source"]})), &media(None, None, None)).is_err());
+  }
+
+  #[test]
+  fn validates_batch_bounds_and_keeps_server_default() {
+    let files = media(None, Some("mf_source"), Some("mf_mask"));
+    for count in [0, 5, u32::MAX] {
+      assert!(build_inpaint_request(&request(json!({"batch_size": count})), &files).is_err());
+    }
+    for count in 1..=4 {
+      assert!(build_inpaint_request(&request(json!({"batch_size": count})), &files).is_ok());
+    }
+    assert!(build_inpaint_request(&request(json!({})), &files).unwrap().num_images.is_none());
+  }
+
+  fn request(value: serde_json::Value) -> TauriGenerateImageRequest {
+    serde_json::from_value(value).unwrap()
+  }
+
+  fn media(scene: Option<&str>, canvas: Option<&str>, mask: Option<&str>) -> SemanticMediaFiles {
+    SemanticMediaFiles { scene_image_media_token: scene.map(MediaFileToken::new_from_str), canvas_image_media_token: canvas.map(MediaFileToken::new_from_str), inpainting_mask_image_media_token: mask.map(MediaFileToken::new_from_str) }
+  }
+}
diff --git a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/mod.rs b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/mod.rs
--- a/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/mod.rs
+++ b/crates/desktop/artcraft/src/core/commands/generate/generate_image/providers/artcraft/legacy/mod.rs
@@ -2,3 +2,4 @@ pub mod artcraft_flux_2_lora_angles;
 pub mod artcraft_flux_dev_juggernaut_inpaint;
 pub mod artcraft_flux_pro_kontext_edit;
 pub mod artcraft_qwen_edit_2511_angles;
+pub mod artcraft_flux_pro_1_inpaint;
diff --git a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx
--- a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx
+++ b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx
@@ -50,6 +50,7 @@ import {
 } from "./components/Model3DOverlay";
 import { CameraIcon, Maximize2Icon, RefreshCwIcon } from "lucide-react";
 import type { PageDrawAdapter } from "./adapter";
+import { useGenerationSubmission } from "./hooks/useGenerationSubmission";
 
 const PAGE_ID: ModelPage = ModelPage.Canvas2D;
 
@@ -417,11 +418,9 @@ const PageDraw = ({
   const baseImageUrl = baseImageInfo?.url;
 
   // Synchronous re-entry guard for the Generate flow. A ref (not state) is the
-  // safety belt against duplicate paid generations: it flips before the first
-  // await so a panic-clicked second click sees the in-flight value immediately,
-  // independent of React state batching. The state below is only for the UI.
-  const generateInFlightRef = useRef(false);
-  const [isGenerating, setIsGenerating] = useState(false);
+  // safety belt against duplicate paid generations: the hook locks before the
+  // first await, reports failures, and releases the button for another attempt.
+  const { isGenerating, runGeneration } = useGenerationSubmission();
 
   const selectedImageModel: ImageModel | undefined =
     useSelectedImageModel(PAGE_ID);
@@ -828,10 +827,7 @@ const PageDraw = ({
         selectedProvider?: GenerationProvider;
       },
     ) => {
-      if (generateInFlightRef.current) return;
-      generateInFlightRef.current = true;
-      setIsGenerating(true);
-      try {
+      await runGeneration(async () => {
         // Yield once so React paints the disabled button before the heavy
         // canvas readback / encode / upload runs and locks the main thread.
         await new Promise<void>((resolve) =>
@@ -954,15 +950,14 @@ const PageDraw = ({
             provider: options?.selectedProvider,
           });
         }
-        if (result?.status === "success") {
-          addPendingGeneration(subscriberId, generationCount);
+        if (result?.status !== "success") {
+          throw result;
         }
-      } finally {
-        generateInFlightRef.current = false;
-        setIsGenerating(false);
-      }
+        addPendingGeneration(subscriberId, generationCount);
+      });
     },
     [
+      runGeneration,
       generationCount,
       getCompositeFile,
       getMaskArrayBuffer,
diff --git a/frontend/libs/components/pagedraw/src/lib/hooks/useGenerationSubmission.spec.ts b/frontend/libs/components/pagedraw/src/lib/hooks/useGenerationSubmission.spec.ts
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/pagedraw/src/lib/hooks/useGenerationSubmission.spec.ts
@@ -0,0 +1,50 @@
+import { act, renderHook } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import toast from "react-hot-toast";
+import { useGenerationSubmission } from "./useGenerationSubmission";
+
+vi.mock("react-hot-toast", () => ({ default: { error: vi.fn() } }));
+
+afterEach(() => vi.restoreAllMocks());
+
+describe("canvas generation submission", () => {
+  it.each([
+    [{ error_message: "Provider rejected the mask" }, "Provider rejected the mask"],
+    [new Error("Mask encoding failed"), "Mask encoding failed"],
+    ["Upload failed", "Upload failed"],
+    [{ status: "server_error" }, "Failed to start image generation. Please try again."],
+    [{ error_message: { unexpected: true } }, "Failed to start image generation. Please try again."],
+  ])("shows failures and releases the generate button", async (error, expected) => {
+    vi.spyOn(console, "error").mockImplementation(() => undefined);
+    const { result } = renderHook(() => useGenerationSubmission());
+    const addPendingGeneration = vi.fn();
+    await act(async () => {
+      await result.current.runGeneration(async () => {
+        await Promise.reject(error);
+        addPendingGeneration();
+      });
+    });
+    expect(toast.error).toHaveBeenCalledWith(expected);
+    expect(addPendingGeneration).not.toHaveBeenCalled();
+    expect(result.current.isGenerating).toBe(false);
+
+    await act(async () => {
+      await result.current.runGeneration(async () => { addPendingGeneration(); });
+    });
+    expect(addPendingGeneration).toHaveBeenCalledOnce();
+  });
+
+  it("prevents duplicate enqueues while a submission is pending", async () => {
+    const { result } = renderHook(() => useGenerationSubmission());
+    let resolve!: () => void;
+    const pending = new Promise<void>((done) => { resolve = done; });
+    const enqueue = vi.fn(() => pending);
+    let first!: Promise<void>;
+    act(() => { first = result.current.runGeneration(enqueue); });
+    expect(result.current.isGenerating).toBe(true);
+    await act(async () => { await result.current.runGeneration(enqueue); });
+    expect(enqueue).toHaveBeenCalledOnce();
+    await act(async () => { resolve(); await first; });
+    expect(result.current.isGenerating).toBe(false);
+  });
+});
diff --git a/frontend/libs/components/pagedraw/src/lib/hooks/useGenerationSubmission.ts b/frontend/libs/components/pagedraw/src/lib/hooks/useGenerationSubmission.ts
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/pagedraw/src/lib/hooks/useGenerationSubmission.ts
@@ -0,0 +1,38 @@
+import { useCallback, useRef, useState } from "react";
+import toast from "react-hot-toast";
+
+const FALLBACK_MESSAGE = "Failed to start image generation. Please try again.";
+
+/** Keep failed canvas submissions visible and allow a subsequent retry. */
+export function useGenerationSubmission() {
+  const inFlight = useRef(false);
+  const [isGenerating, setIsGenerating] = useState(false);
+
+  const runGeneration = useCallback(async (submit: () => Promise<void>) => {
+    if (inFlight.current) return;
+    inFlight.current = true;
+    setIsGenerating(true);
+    try {
+      await submit();
+    } catch (error) {
+      console.error("Canvas generation failed", error);
+      toast.error(generationErrorMessage(error));
+    } finally {
+      inFlight.current = false;
+      setIsGenerating(false);
+    }
+  }, []);
+
+  return { isGenerating, runGeneration };
+}
+
+function generationErrorMessage(error: unknown): string {
+  if (typeof error === "string" && error.trim()) return error;
+  if (error && typeof error === "object") {
+    const details = error as { error_message?: unknown; message?: unknown };
+    for (const message of [details.error_message, details.message]) {
+      if (typeof message === "string" && message.trim()) return message;
+    }
+  }
+  return FALLBACK_MESSAGE;
+}
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,7 @@
+# The `storyteller-docs` Netlify site still builds this repository, but its
+# build command (./script/netlify_build_docs.sh) and the docs themselves now
+# live in storytold/artcraft-services, so every deploy preview here fails.
+# Skip those builds until the Netlify site is repointed or disconnected, then
+# delete this file.
+[build]
+  ignore = "exit 0"
