diff --git a/backend/services/projects.py b/backend/services/projects.py index 3ae301c..d8b0a1a 100644 --- a/backend/services/projects.py +++ b/backend/services/projects.py @@ -201,10 +201,24 @@ def _read_meta_with_generation( return ProjectMeta.model_validate(data), gen -def _write_meta(storage: StorageBackend, meta: ProjectMeta) -> None: +def _write_meta( + storage: StorageBackend, + meta: ProjectMeta, + *, + owner_user_id: str | None = None, +) -> None: + """Persist meta under ``users/{owner}/projects/{id}/``. + + ``meta.user_id`` can lag the storage prefix (local-auth accounts that + still have ``user_id: "local"`` in JSON while files live under + ``users/usr_…/``). Writes must follow the prefix used to *read* the + project, not the stale field — otherwise ``update_project(pcb_status=…)`` + lands in a different tree and the PCB worker still sees ``draft``. + """ + uid = owner_user_id or meta.user_id meta.updated = datetime.now(timezone.utc).isoformat() storage.write_json( - _meta_key(meta.user_id, meta.id), + _meta_key(uid, meta.id), meta.model_dump(), ) @@ -516,7 +530,7 @@ def update_project( meta = _read_meta(storage, user_id, project_id) for k, v in fields.items(): setattr(meta, k, v) - _write_meta(storage, meta) + _write_meta(storage, meta, owner_user_id=user_id) return meta @@ -691,7 +705,7 @@ def add_collaborator( meta = _read_meta(storage, owner_user_id, project_id) if collaborator_user_id not in meta.collaborators: meta.collaborators.append(collaborator_user_id) - _write_meta(storage, meta) + _write_meta(storage, meta, owner_user_id=owner_user_id) # Write reverse reference for the collaborator ref_key = _shared_ref_key(collaborator_user_id, project_id) storage.write_json(ref_key, {"owner_user_id": owner_user_id}) @@ -704,7 +718,7 @@ def remove_collaborator( """Remove a collaborator from a project and delete the shared reference.""" meta = _read_meta(storage, owner_user_id, project_id) meta.collaborators = [c for c in meta.collaborators if c != collaborator_user_id] - _write_meta(storage, meta) + _write_meta(storage, meta, owner_user_id=owner_user_id) # Delete reverse reference ref_key = _shared_ref_key(collaborator_user_id, project_id) if storage.exists(ref_key): diff --git a/frontend/src/app/(app)/project/[id]/pcb/page.tsx b/frontend/src/app/(app)/project/[id]/pcb/page.tsx index 0d89937..74d91b8 100644 --- a/frontend/src/app/(app)/project/[id]/pcb/page.tsx +++ b/frontend/src/app/(app)/project/[id]/pcb/page.tsx @@ -28,6 +28,7 @@ export default function PcbReviewPage({ const { id } = use(params); const [projectName, setProjectName] = useState(""); const [pcbStatus, setPcbStatus] = useState("draft"); + const [analysisBusy, setAnalysisBusy] = useState(false); const [inventory, setInventory] = useState<{ nets: Array>; domains: string[]; @@ -49,6 +50,7 @@ export default function PcbReviewPage({ .then((p) => { setProjectName(p.name); setPcbStatus(p.pcbStatus ?? "draft"); + setAnalysisBusy(p.status === "running" || p.status === "queued"); setStatusLoaded(true); }) .catch(() => setStatusLoaded(true)); @@ -198,8 +200,19 @@ export default function PcbReviewPage({ )} + {statusLoaded && pcbStatus === "draft" && analysisBusy && ( +

+ Schematic review is still running. Wait for it to finish (or cancel + it), then start PCB review — the two jobs cannot run at the same time. +

+ )} + {statusLoaded && pcbStatus === "draft" && ( - diff --git a/tests/test_pcb_review.py b/tests/test_pcb_review.py index c210aac..e6c3c5e 100644 --- a/tests/test_pcb_review.py +++ b/tests/test_pcb_review.py @@ -181,3 +181,33 @@ def test_pcb_start_requires_board(tmp_path: Path): resp = client.post(f"/api/pipeline/{pid}/pcb/start") assert resp.status_code == 400 assert "kicad_pcb" in resp.json()["detail"] + + +def test_update_project_writes_storage_prefix_not_stale_user_id(tmp_path: Path): + """JWT owner path with JSON still saying user_id=local (live Emmaforo).""" + from backend.services.storage import LocalStorageBackend + + storage = LocalStorageBackend(tmp_path) + pid = "759a4dea9612" + owner = "usr_jwt_owner" + storage.write_json( + f"users/{owner}/projects/{pid}/project.json", + { + "id": pid, + "name": "Emmaforo", + "user_id": "local", + "created": "2026-01-01T00:00:00Z", + "status": "complete", + "has_pcb": True, + "has_bom": True, + "has_netlist": True, + "pcb_status": "draft", + }, + ) + from backend.services import projects as proj_svc + + proj_svc.update_project(storage, owner, pid, pcb_status="queued") + jwt_meta = proj_svc.get_project(storage, owner, pid) + assert jwt_meta is not None + assert jwt_meta.pcb_status == "queued" + assert proj_svc.get_project(storage, "local", pid) is None