Write project.json to the storage owner prefix.
update_project used meta.user_id for the path, so JWT projects that still have user_id=local never persisted pcb_status=queued and the PCB worker exited as draft. Point writes at the prefix used to read the project.
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function PcbReviewPage({
|
||||
const { id } = use(params);
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [pcbStatus, setPcbStatus] = useState<string>("draft");
|
||||
const [analysisBusy, setAnalysisBusy] = useState(false);
|
||||
const [inventory, setInventory] = useState<{
|
||||
nets: Array<Record<string, unknown>>;
|
||||
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({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{statusLoaded && pcbStatus === "draft" && analysisBusy && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{statusLoaded && pcbStatus === "draft" && (
|
||||
<Button size="sm" disabled={starting} onClick={handleStart}>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={starting || analysisBusy}
|
||||
onClick={handleStart}
|
||||
>
|
||||
<CircuitBoard className="h-4 w-4 mr-1" />
|
||||
{starting ? "Starting…" : "Run PCB review"}
|
||||
</Button>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user