fix: file display, persistence, PDF support and save error handling

- Add file proxy route in maker blueprint (X-API-Key auth for browser requests)
- Persist file_path/annotations_json to DB via RecipeCreate/RecipeUpdate schemas
- Fix canvas sizing using grandparent container instead of Fabric.js wrapper div
- Defer canvas init with requestAnimationFrame for x-show timing
- Add PDF.js support in annotation-editor and annotation-viewer
- Fix annotations_json double-serialization (parse string to object before send)
- Handle FastAPI 422 validation error arrays in api_client and JS error display
- Update template URLs to use /maker/api/files/ proxy path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 22:04:45 +01:00
parent d262ef68af
commit b075115cef
8 changed files with 315 additions and 65 deletions
+8
View File
@@ -13,6 +13,10 @@ class RecipeCreate(BaseModel):
code: str = Field(..., min_length=1, max_length=100)
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
# Optional task-level fields for the initial technical drawing
file_path: Optional[str] = Field(None, max_length=500)
file_type: Optional[str] = Field(None, pattern="^(image|pdf)$")
annotations_json: Optional[dict] = None
class RecipeUpdate(BaseModel):
@@ -20,6 +24,10 @@ class RecipeUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
change_notes: Optional[str] = None
# Task-level fields: saved to the first task of the new version
file_path: Optional[str] = Field(None, max_length=500)
file_type: Optional[str] = Field(None, pattern="^(image|pdf)$")
annotations_json: Optional[dict] = None
class RecipeVersionResponse(BaseModel):
+56
View File
@@ -147,6 +147,22 @@ async def create_recipe(
db.add(version)
await db.flush()
# Create initial task with technical drawing if file was uploaded
if data.file_path:
initial_task = RecipeTask(
version_id=version.id,
order_index=0,
title="Technical Drawing",
file_path=data.file_path,
file_type=data.file_type or (
"pdf" if data.file_path.lower().endswith(".pdf")
else "image"
),
annotations_json=data.annotations_json,
)
db.add(initial_task)
await db.flush()
await _write_audit(
db,
recipe_id=recipe.id,
@@ -223,6 +239,46 @@ async def create_new_version(
# Deep-copy tasks + subtasks
await _copy_tasks_to_version(db, source_version=current, target_version=new_version)
# Apply task-level updates (file_path, annotations_json) to the first task
if data.file_path is not None or data.annotations_json is not None:
# Reload new version's tasks
result_tasks = await db.execute(
select(RecipeTask)
.where(RecipeTask.version_id == new_version.id)
.order_by(RecipeTask.order_index)
)
tasks = list(result_tasks.scalars().all())
if tasks:
first_task = tasks[0]
if data.file_path is not None:
first_task.file_path = data.file_path
# Auto-detect file_type from extension
if data.file_type:
first_task.file_type = data.file_type
elif data.file_path.lower().endswith(".pdf"):
first_task.file_type = "pdf"
else:
first_task.file_type = "image"
if data.annotations_json is not None:
first_task.annotations_json = data.annotations_json
else:
# No tasks yet — create a default task to hold the drawing
default_task = RecipeTask(
version_id=new_version.id,
order_index=0,
title="Technical Drawing",
file_path=data.file_path,
file_type=data.file_type or (
"pdf" if data.file_path and data.file_path.lower().endswith(".pdf")
else "image"
),
annotations_json=data.annotations_json,
)
db.add(default_task)
await db.flush()
# Apply header updates
update_fields: dict = {}
if data.name is not None: