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:
+1
-1
@@ -52,7 +52,7 @@ class Settings(BaseSettings):
|
||||
"""Absolute path to upload directory."""
|
||||
return Path(__file__).parent / self.upload_dir
|
||||
|
||||
model_config = {"env_file": "../.env", "env_file_encoding": "utf-8"}
|
||||
model_config = {"env_file": "../.env", "env_file_encoding": "utf-8", "extra": "ignore"}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -93,10 +93,19 @@ async def update_recipe(
|
||||
user: User = Depends(require_maker),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update a recipe - creates a new version via copy-on-write. Requires Maker role."""
|
||||
new_version = await recipe_service.create_new_version(db, recipe_id, data, user)
|
||||
"""Update a recipe. Creates a new version only if the current one has measurements.
|
||||
Otherwise updates in-place. Requires Maker role.
|
||||
"""
|
||||
current = await recipe_service.get_current_version(db, recipe_id)
|
||||
has_measurements = await recipe_service.version_has_measurements(db, current.id)
|
||||
|
||||
if has_measurements:
|
||||
# Copy-on-write: create new version to preserve measurement data
|
||||
await recipe_service.create_new_version(db, recipe_id, data, user)
|
||||
else:
|
||||
# No measurements yet: update in-place
|
||||
await recipe_service.update_current_version(db, recipe_id, data)
|
||||
|
||||
# Reload full recipe for consistent response
|
||||
recipe = await recipe_service.get_recipe_detail(db, recipe_id)
|
||||
return _recipe_to_response(recipe)
|
||||
|
||||
|
||||
+13
-5
@@ -153,11 +153,19 @@ async def create_task(
|
||||
detail="Cannot modify an inactive recipe",
|
||||
)
|
||||
|
||||
# Copy-on-write: create new version with existing tasks cloned
|
||||
from schemas.recipe import RecipeUpdate
|
||||
new_version = await recipe_service.create_new_version(
|
||||
db, recipe_id, RecipeUpdate(change_notes=f"Added task: {data.title}"), user
|
||||
)
|
||||
# Check if current version has measurements
|
||||
current = await recipe_service.get_current_version(db, recipe_id)
|
||||
has_measurements = await recipe_service.version_has_measurements(db, current.id)
|
||||
|
||||
if has_measurements:
|
||||
# Copy-on-write: create new version preserving measurement data
|
||||
from schemas.recipe import RecipeUpdate
|
||||
new_version = await recipe_service.create_new_version(
|
||||
db, recipe_id, RecipeUpdate(change_notes=f"Added task: {data.title}"), user
|
||||
)
|
||||
else:
|
||||
# No measurements: use current version directly
|
||||
new_version = current
|
||||
|
||||
# Determine order_index (append at end)
|
||||
max_order = max((t.order_index for t in new_version.tasks), default=-1)
|
||||
|
||||
@@ -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