feat: measurement workflow improvements and recipe update-in-place
- Auto-advance to next task after completing all subtask measurements - 1s pause between measurements to show pass/fail/warning result - Colored marker strip (green/red/amber) based on measurement status - Replace duplicate measurements instead of appending (fixes progress bar) - Add Task column and Date/Time column to measurement summary table - Enrich summary with task_info for each measurement - Update-in-place for recipe versions without measurements (no copy-on-write) - Dark theme improvements and navbar cleanup - Server config: ignore extra env vars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -357,6 +357,82 @@ async def get_measurement_count(
|
||||
return count_result.scalar_one()
|
||||
|
||||
|
||||
async def version_has_measurements(db: AsyncSession, version_id: int) -> bool:
|
||||
"""Check if any measurements exist for the given version."""
|
||||
result = await db.execute(
|
||||
select(func.count()).select_from(Measurement).where(
|
||||
Measurement.version_id == version_id
|
||||
)
|
||||
)
|
||||
return result.scalar_one() > 0
|
||||
|
||||
|
||||
async def update_current_version(
|
||||
db: AsyncSession,
|
||||
recipe_id: int,
|
||||
data: RecipeUpdate,
|
||||
) -> RecipeVersion:
|
||||
"""Update recipe header in-place on the current version (no copy-on-write)."""
|
||||
# Apply header updates (name, description)
|
||||
update_fields: dict = {}
|
||||
if data.name is not None:
|
||||
update_fields["name"] = data.name
|
||||
if data.description is not None:
|
||||
update_fields["description"] = data.description
|
||||
if update_fields:
|
||||
await db.execute(
|
||||
update(Recipe).where(Recipe.id == recipe_id).values(**update_fields)
|
||||
)
|
||||
|
||||
# Apply task-level file updates to first task (same logic as create_new_version)
|
||||
current = await _get_current_version(db, recipe_id)
|
||||
if data.file_path is not None or data.annotations_json is not None:
|
||||
result_tasks = await db.execute(
|
||||
select(RecipeTask)
|
||||
.where(RecipeTask.version_id == current.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
|
||||
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=current.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()
|
||||
|
||||
# Reload version with tasks for response
|
||||
result = await db.execute(
|
||||
select(RecipeVersion)
|
||||
.where(RecipeVersion.id == current.id)
|
||||
.options(
|
||||
selectinload(RecipeVersion.tasks).selectinload(RecipeTask.subtasks)
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def list_recipes(
|
||||
db: AsyncSession,
|
||||
page: int = 1,
|
||||
|
||||
Reference in New Issue
Block a user