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:
Adriano
2026-02-09 19:36:42 +01:00
parent 4854966bf7
commit e8b88d48c1
9 changed files with 232 additions and 118 deletions
+76
View File
@@ -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,