# Docker Production Error Report — 2026-04-19 Errors extracted from `docker logs velxio-app` (last 24h). Compilation errors from user code (syntax, missing libraries) are excluded — those are expected. --- ## 1. Raspberry Pi 3 — Missing QEMU boot files **Occurrences:** 46 (raspberry-pi-3) + 5 (raspberry-pi-3-2) = **51 total** **Severity:** HIGH — feature completely broken, users see error **User impact:** Anyone selecting Raspberry Pi 3 board gets immediate failure ### Log sample ``` INFO: ('172.20.0.1', 0) - "WebSocket /api/simulation/ws/raspberry-pi-3" [accepted] INFO: connection open ERROR app.services.qemu_manager: Missing QEMU images in /app/app/services/../../../img ERROR app.api.routes.simulation: [raspberry-pi-3] error: Missing QEMU boot files (kernel8.img / SD image) ``` ### Root cause The QEMU manager expects boot files (`kernel8.img`, SD image) in `/app/app/services/../../../img` (resolves to `/img` in the container). These files are not included in the Docker image and no download mechanism exists for them. ### Affected files - `backend/app/services/qemu_manager.py` — checks for image directory - `backend/app/api/routes/simulation.py` — reports the error to the WebSocket client ### Suggested fix Option A: Add `kernel8.img` and SD image to the Docker build (similar to ESP32 ROM files). Option B: Hide/disable Raspberry Pi 3 from the board selector until images are available. Option C: Return a clear user-facing error message ("Raspberry Pi 3 simulation is not yet available") instead of a generic failure. --- ## 2. ESP32-S3 — Unsupported QEMU machine type **Occurrences:** 5 worker crashes (2 unique sessions) **Severity:** HIGH — feature completely broken, will never work with current QEMU lib **User impact:** Anyone selecting ESP32-S3 board gets simulation failure ### Log sample ``` INFO app.api.routes.simulation: [b923990f-...::esp32-s3] start_esp32 board=esp32-s3 firmware=4096KB lib_available=True INFO app.services.esp32_lib_manager: Launching esp32_worker (machine=esp32s3-picsimlab, ...) INFO app.services.esp32_lib_manager: [worker:...] [esp32_worker] Loading library: /app/lib/libqemu-xtensa.so INFO app.services.esp32_lib_manager: [worker:...] qemu: unsupported machine type INFO app.services.esp32_lib_manager: [worker:...] Use -machine help to list supported machines WARNING app.services.esp32_lib_manager: [...::esp32-s3] worker exited unexpectedly (code 1) ``` ### Root cause The pre-built `libqemu-xtensa.so` (lcgamboa/picsimlab fork) only supports `esp32-picsimlab` machine type. The code tries to launch `esp32s3-picsimlab` which does not exist in this QEMU build. ESP32-S3 uses Xtensa LX7 (vs LX6 for vanilla ESP32) — the current QEMU binary does not emulate LX7. ### Affected files - `backend/app/services/esp32_lib_manager.py` — maps board to QEMU machine name - `prebuilt/qemu/` — pre-built QEMU binaries ### Suggested fix Option A: Compile a QEMU build that includes ESP32-S3 support (if available in lcgamboa fork). Option B: Hide ESP32-S3 from the board selector or show "simulation not supported" message. Option C: Fall back gracefully instead of crashing the worker — return an error to the WebSocket. --- ## 3. WebSocket — "accept" race condition after connection close **Occurrences:** 7 errors across 5 unique sessions **Severity:** MEDIUM — does not crash uvicorn currently, but same class of bug that caused the 31-hour outage on 2026-04-16 (AssertionError in websockets keepalive_ping) **User impact:** None visible (connection already closed), but risks process crash ### Log sample ``` INFO: connection closed INFO app.services.esp32_lib_manager: WorkerInstance 9df41c8f-...::esp32 shut down ERROR app.api.routes.simulation: WebSocket error for 9df41c8f-...::esp32: WebSocket is not connected. Need to call "accept" first. ``` ### Root cause After the WebSocket connection is closed (client navigates away or network drop), the simulation route code still attempts to send data or perform operations on the WebSocket object. The error message "Need to call accept first" indicates the WebSocket was never fully accepted or was already disconnected when a send was attempted. ### Affected files - `backend/app/api/routes/simulation.py` — WebSocket handler, cleanup logic ### Suggested fix Wrap WebSocket send operations in try/except to catch `WebSocketDisconnect` and connection errors. Check `websocket.client_state` before attempting sends. Ensure the cleanup path does not attempt to write to an already-closed connection. Example: ```python try: await websocket.send_json(data) except (WebSocketDisconnect, RuntimeError): # Client already gone — clean up silently break ``` --- ## 4. Google OAuth — Unhandled 400 from token exchange **Occurrences:** 1 **Severity:** MEDIUM — returns 500 Internal Server Error to the user instead of a friendly redirect **User impact:** User sees a blank error page instead of being redirected to login ### Log sample ``` INFO httpx: HTTP Request: POST https://oauth2.googleapis.com/token "HTTP/1.1 400 Bad Request" INFO: 172.20.0.1:0 - "GET /api/auth/google/callback?code=4/0Aci98E8...&scope=...&authuser=0 HTTP/1.0" 500 Internal Server Error ERROR: Exception in ASGI application Traceback (most recent call last): ... File "/app/app/api/routes/auth.py", line 119, in google_callback token_resp.raise_for_status() ... httpx.HTTPStatusError: Client error '400 Bad Request' for url 'https://oauth2.googleapis.com/token' ``` ### Root cause The `google_callback` endpoint in `auth.py:119` calls `token_resp.raise_for_status()` without try/except. When Google rejects the authorization code (expired, already used, or user reloaded the callback page), the unhandled exception propagates to a 500 error. ### Affected file - `backend/app/api/routes/auth.py` — line 119 ### Suggested fix ```python # In google_callback(): token_resp = await client.post(GOOGLE_TOKEN_URL, data={...}) if token_resp.status_code != 200: return RedirectResponse(url=f"{settings.FRONTEND_URL}/login?error=google_auth_failed") access_token = token_resp.json()["access_token"] ``` --- ## 5. Uvicorn crash from websockets keepalive (RESOLVED) **Occurrences:** 1 (caused ~31h outage on 2026-04-16) **Severity:** CRITICAL — killed the entire backend process **Status:** MITIGATED — `deploy/entrypoint.sh` now uses `wait -n` so Docker restarts the container automatically. Root cause (old `websockets` library) still present. ### Log sample ``` ERROR: keepalive ping failed Traceback (most recent call last): File ".../websockets/legacy/protocol.py", line 1233, in keepalive_ping pong_waiter = await self.ping() File ".../websockets/legacy/protocol.py", line 308, in _drain_helper assert waiter is None or waiter.cancelled() AssertionError ``` ### Root cause Race condition in `websockets` (legacy branch) when a connection closes while a keepalive ping is in flight. The assertion in `_drain_helper` fails and the unhandled exception kills uvicorn. ### Fix applied - `deploy/entrypoint.sh` — changed to monitor both uvicorn and nginx with `wait -n`; if either dies, the container exits and Docker's `restart: unless-stopped` policy recovers it. Commit: `8e3c00e` (2026-04-16). ### Remaining work - Update `websockets` to >= 12.x in `backend/requirements.txt` (fixes the race condition). - Update `uvicorn[standard]` to latest (moves off `websockets/legacy` handler). --- ## Summary table | # | Problem | Occurrences (24h) | Severity | Status | |---|---------|-------------------|----------|--------| | 1 | RPi3 missing QEMU boot files | 51 | HIGH | Open | | 2 | ESP32-S3 unsupported QEMU machine | 5 | HIGH | Open | | 3 | WebSocket send after close | 7 | MEDIUM | Open | | 4 | Google OAuth unhandled 400 | 1 | MEDIUM | Open | | 5 | Uvicorn crash from websockets keepalive | 1 | CRITICAL | Mitigated |