robopark 2.8.21 → 2.8.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robopark",
3
- "version": "2.8.21",
3
+ "version": "2.8.22",
4
4
  "description": "RoboPark fleet control CLI — set up, watch, and drive a fleet of talking robots. The operator front-end over the infinicode mesh.",
5
5
  "type": "module",
6
6
  "bin": {
package/scheduler/main.py CHANGED
@@ -55,6 +55,12 @@ class Robot(BaseModel):
55
55
  last_heartbeat: Optional[datetime] = None
56
56
  total_sessions: int = 0
57
57
  total_runtime_seconds: int = 0
58
+ # Added after the `robots` table's additive trigger_count/created_at
59
+ # migration — response_model=Robot on GET /api/robots(/{id}) was
60
+ # silently stripping both from every response since the DB row has
61
+ # them but this model didn't declare them.
62
+ trigger_count: int = 0
63
+ created_at: Optional[datetime] = None
58
64
 
59
65
  class LiveKitServer(BaseModel):
60
66
  id: str
@@ -1589,10 +1595,16 @@ async def update_device(device_id: str, payload: DeviceUpdate):
1589
1595
  await _log_history("device", "device", device_id, "updated", "operator", f"fields={','.join(fields.keys())}")
1590
1596
  return await get_device(device_id)
1591
1597
 
1592
- @app.post("/api/devices", response_model=Device)
1598
+ @app.post("/api/devices")
1593
1599
  async def create_device(payload: DeviceCreate):
1594
1600
  """Manually register a device from the UI. Returns the device record and
1595
- a one-time enrollment token (if one wasn't provided)."""
1601
+ a one-time enrollment token (if one wasn't provided).
1602
+
1603
+ BUG FIXED: this had response_model=Device, which made FastAPI silently
1604
+ strip the enrollment_token key from the response below (Device has no
1605
+ such field) — the docstring's promised token was never actually
1606
+ reaching the caller. Dropping response_model here since the return
1607
+ value is deliberately Device-plus-one-extra-field, not just Device."""
1596
1608
  if not payload.name or not payload.name.strip():
1597
1609
  raise HTTPException(400, "name is required")
1598
1610
 
@@ -1763,6 +1775,17 @@ async def enroll_device(payload: DeviceEnrollRequest):
1763
1775
  )
1764
1776
  else:
1765
1777
  # First-boot enroll via the global default token -> create a new device.
1778
+ #
1779
+ # BUG FIXED: this used to persist enrollment_token_hash=token_hash
1780
+ # (the hash of the SHARED default token) onto the new device row.
1781
+ # Since the "existing" lookup above matches on enrollment_token_hash,
1782
+ # every subsequent enroll using that same shared default token would
1783
+ # match THIS row and take the "existing" reuse/rotate path instead of
1784
+ # minting an independent device — silently renaming and re-keying
1785
+ # whichever device happened to enroll first, and invalidating its
1786
+ # live device_token. Devices minted via the default token must keep
1787
+ # enrollment_token_hash NULL so the default token stays reusable
1788
+ # across an entire fleet, as its name implies.
1766
1789
  device_id = f"dev_{secrets.token_hex(4)}"
1767
1790
  new_token = secrets.token_urlsafe(32)
1768
1791
  new_hash = _hash(new_token)
@@ -1779,7 +1802,7 @@ async def enroll_device(payload: DeviceEnrollRequest):
1779
1802
  payload.tailscale_ip, payload.lan_ip,
1780
1803
  payload.motor_server_url, payload.character_id,
1781
1804
  payload.livekit_url, payload.video_device, payload.audio_device,
1782
- new_hash, token_hash, now, now,
1805
+ new_hash, None, now, now,
1783
1806
  ),
1784
1807
  )
1785
1808
  await db.execute(
@@ -190,10 +190,18 @@ class PreviewAgent:
190
190
  logger.error("no DEVICE_TOKEN and no ENROLLMENT_TOKEN; cannot poll scheduler")
191
191
  sys.exit(1)
192
192
 
193
+ self._session = httpx.AsyncClient()
194
+
195
+ # Must come after self._session exists — _resolve_device_id() guards
196
+ # on it and silently no-ops otherwise. On a fresh enroll (no cached
197
+ # device_id in ~/.robopark/preview_agent.json) that made this ALWAYS
198
+ # no-op, so device_id never resolved and preview/agent polling fell
199
+ # back to robot_id (the display name) instead of the real device_id
200
+ # for the entire session — the same id-mismatch bug fixed elsewhere,
201
+ # recurring here only on a first-ever enroll.
193
202
  if not self.device_id:
194
203
  await self._resolve_device_id()
195
204
 
196
- self._session = httpx.AsyncClient()
197
205
  self._loop = asyncio.get_running_loop()
198
206
  self._start_vision_webhook_server()
199
207