Skip to content

feat(sessions): add active_camera property for Python control#837

Merged
PythonFZ merged 3 commits intomainfrom
feat/active-camera-per-session
Jan 13, 2026
Merged

feat(sessions): add active_camera property for Python control#837
PythonFZ merged 3 commits intomainfrom
feat/active-camera-per-session

Conversation

@PythonFZ
Copy link
Copy Markdown
Member

@PythonFZ PythonFZ commented Jan 11, 2026

Add the ability to control which camera a frontend session views through from Python via vis.sessions["session_id"].active_camera = "camera_key".

Backend:

  • Add REST endpoints GET/PUT for /api/rooms/{room}/sessions/{session}/active-camera
  • Add ACTIVE_CAMERA_UPDATE socket event for Python -> Frontend sync
  • Initialize active_camera to session's own camera on join, cleanup on disconnect
  • Add validation: KeyError for missing camera, TypeError for non-Camera geometry

Frontend:

  • Handle active_camera:update socket event
  • Sync sidebar camera changes to backend via REST PUT
  • Fix auth token retrieval (was using wrong localStorage key)

Python API:

  • session.active_camera: get/set which camera to view through
  • session.camera: now gets/sets the currently active camera geometry

Summary by CodeRabbit

  • New Features

    • Active camera selection: participants can choose and switch cameras; selections sync in real time and persist across reconnects.
  • Behavior

    • Per-session isolation: each session maintains its own independent active camera.
    • Camera changes propagate to connected clients immediately and are persisted so reconnecting clients restore the chosen camera.
  • Tests

    • Added integration tests covering selection, persistence, isolation, and error cases.

✏️ Tip: You can customize this high-level summary in your review settings.

Add the ability to control which camera a frontend session views through
from Python via `vis.sessions["session_id"].active_camera = "camera_key"`.

Backend:
- Add REST endpoints GET/PUT for /api/rooms/{room}/sessions/{session}/active-camera
- Add ACTIVE_CAMERA_UPDATE socket event for Python -> Frontend sync
- Initialize active_camera to session's own camera on join, cleanup on disconnect
- Add validation: KeyError for missing camera, TypeError for non-Camera geometry

Frontend:
- Handle active_camera:update socket event
- Sync sidebar camera changes to backend via REST PUT
- Fix auth token retrieval (was using wrong localStorage key)

Python API:
- session.active_camera: get/set which camera to view through
- session.camera: now gets/sets the currently active camera geometry

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Adds per-session "active camera" support: frontend socket handler and store setter, client PUT/GET endpoints to persist session active camera in Redis, server-side session key helpers and events to initialize/cleanup active camera, session manager property for active_camera, and integration tests.

Changes

Cohort / File(s) Summary
Frontend Socket & Store
app/src/hooks/useSocketManager.ts, app/src/store.tsx
Register/unregister active_camera:update socket event; add setAttachedCameraKey(cameraKey) to store; attachToCamera() now issues PUT /api/rooms/{roomId}/sessions/{sessionId}/active-camera when auth/ids available.
Backend API Manager
src/zndraw/api_manager.py
Added get_active_camera(session_id: str) -> str and set_active_camera(session_id: str, camera_key: str) -> None to fetch/persist a session's active camera via HTTP.
Backend Constants & Redis Keys
src/zndraw/app/constants.py, src/zndraw/app/redis_keys.py
Added ACTIVE_CAMERA_UPDATE = "active_camera:update" constant and RoomKeys.session_active_camera(session_id) producing room:{room_id}:session:{session_id}:active_camera.
Backend Events
src/zndraw/app/events.py
On room join, initialize session active camera to the session's own camera key; on disconnect cleanup, remove session's active camera key.
Session Routes (API)
src/zndraw/app/session_routes.py
Added GET /api/rooms/<room_id>/sessions/<session_id>/active-camera and PUT to set it; PUT validates JSON and active_camera field, writes Redis key, and emits ACTIVE_CAMERA_UPDATE scoped to the session.
Session Manager
src/zndraw/session_manager.py
Added FrontendSession.active_camera property (getter/setter). Setter validates key exists and is a Camera, updates backend via API; camera now reflects active camera semantics.
Tests
tests/test_active_camera.py
New integration tests covering default active camera, persistence, per-session isolation, error cases (missing/non-Camera), and interactions between session.camera and session.active_camera.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend App
    participant API as Server API
    participant Redis as Redis
    participant Socket as SocketIO

    User->>Frontend: attachToCamera(cameraKey)
    Frontend->>Frontend: setAttachedCameraKey(cameraKey)
    Frontend->>API: PUT /api/rooms/{room}/sessions/{session}/active-camera {active_camera: cameraKey}
    API->>Redis: SET room:{room}:session:{session}:active_camera = cameraKey
    API->>Socket: emit ACTIVE_CAMERA_UPDATE to session
    Socket->>Frontend: active_camera:update event
    Frontend->>Frontend: update store (setAttachedCameraKey)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nudged the lens with a curious hop,
A PUT, a ping, and the whiskers stop.
Redis keeps secrets, sockets hum the song,
Now every session knows where views belong. 📷✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(sessions): add active_camera property for Python control' accurately and specifically describes the main change—adding a new active_camera property to sessions for Python control, which aligns with the primary objective and the majority of changes across both backend and frontend.
Docstring Coverage ✅ Passed Docstring coverage is 86.21% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9cd317a and a474208.

📒 Files selected for processing (4)
  • app/src/store.tsx
  • src/zndraw/api_manager.py
  • src/zndraw/app/constants.py
  • src/zndraw/app/events.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/zndraw/app/events.py
  • app/src/store.tsx
  • src/zndraw/api_manager.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: You MUST NEVER @pytest.mark.xfail or similar - all tests must pass!
All default values (e.g., camera, particles) must be defined exclusively within the Pydantic model. Do not scatter fallback logic throughout the codebase.
Do not perform null checks combined with hardcoded literals for default values. Rely entirely on the schema to populate default values during initialization.
You can not use LUA scripts with Redis!
If sensible, implement collections.abc interfaces for your classes, such as MutableMapping or MutableSequence.
Use numpy style docstrings in Python code.
Docstrings must be concise and to the point.
Use type hints wherever possible in Python. Use list[int|float] | None instead of t.Optional[t.List[int|float]]!
Imports should always be at the top of the file in Python.

Files:

  • src/zndraw/app/constants.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: pytest (3.13, ubuntu-latest)
  • GitHub Check: pytest (3.11, ubuntu-latest)
  • GitHub Check: pytest (3.12, ubuntu-latest)
🔇 Additional comments (1)
src/zndraw/app/constants.py (1)

26-26: LGTM!

The new ACTIVE_CAMERA_UPDATE constant follows the established naming conventions and event string format used by other socket events in this class. Clean addition for the active camera synchronization feature.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/zndraw/api_manager.py:
- Around line 1716-1741: The get_active_camera method's return type is
inaccurate because response.json().get("active_camera") may be None; update the
function signature from def get_active_camera(self, session_id: str) -> str: to
return an optional type (e.g., -> str | None or -> Optional[str]) and import
Optional from typing if needed; also update the docstring "Returns" section to
indicate it can return None and ensure any callers handle a None value
appropriately.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 738edfa and 119c1cb.

📒 Files selected for processing (9)
  • app/src/hooks/useSocketManager.ts
  • app/src/store.tsx
  • src/zndraw/api_manager.py
  • src/zndraw/app/constants.py
  • src/zndraw/app/events.py
  • src/zndraw/app/redis_keys.py
  • src/zndraw/app/session_routes.py
  • src/zndraw/session_manager.py
  • tests/test_active_camera.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: You MUST NEVER @pytest.mark.xfail or similar - all tests must pass!
All default values (e.g., camera, particles) must be defined exclusively within the Pydantic model. Do not scatter fallback logic throughout the codebase.
Do not perform null checks combined with hardcoded literals for default values. Rely entirely on the schema to populate default values during initialization.
You can not use LUA scripts with Redis!
If sensible, implement collections.abc interfaces for your classes, such as MutableMapping or MutableSequence.
Use numpy style docstrings in Python code.
Docstrings must be concise and to the point.
Use type hints wherever possible in Python. Use list[int|float] | None instead of t.Optional[t.List[int|float]]!
Imports should always be at the top of the file in Python.

Files:

  • src/zndraw/app/constants.py
  • src/zndraw/api_manager.py
  • tests/test_active_camera.py
  • src/zndraw/app/session_routes.py
  • src/zndraw/app/redis_keys.py
  • src/zndraw/app/events.py
  • src/zndraw/session_manager.py
**/test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

When designing new tests, read the old tests first to understand the existing patterns. Use pytest.mark.parametrize to avoid code duplication. Tests should be very specific and test only one thing. Avoid complex test setups. Each test must be a function, not a method of a class!

Files:

  • tests/test_active_camera.py
🧬 Code graph analysis (3)
app/src/store.tsx (2)
app/src/components/CameraManager.js (1)
  • sessionId (18-18)
app/src/utils/auth.ts (1)
  • getToken (152-154)
src/zndraw/app/events.py (2)
src/zndraw/app/redis_keys.py (2)
  • session_active_camera (345-360)
  • session_id (617-619)
src/zndraw/session_manager.py (1)
  • get_session_camera_key (16-18)
src/zndraw/session_manager.py (3)
src/zndraw/geometries/camera.py (1)
  • Camera (27-223)
src/zndraw/api_manager.py (2)
  • get_active_camera (1716-1741)
  • set_active_camera (1743-1765)
src/zndraw/app/session_routes.py (2)
  • get_active_camera (145-166)
  • set_active_camera (174-217)
🪛 Ruff (0.14.10)
src/zndraw/session_manager.py

172-172: Avoid specifying long messages outside the exception class

(TRY003)


177-179: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: pytest (3.11, ubuntu-latest)
  • GitHub Check: pytest (3.12, ubuntu-latest)
  • GitHub Check: pytest (3.13, ubuntu-latest)
🔇 Additional comments (25)
src/zndraw/app/constants.py (1)

26-26: LGTM!

The new ACTIVE_CAMERA_UPDATE constant follows the established naming convention and is consistent with other socket event names in the class.

src/zndraw/app/events.py (2)

310-312: LGTM!

The active camera key cleanup is correctly placed alongside other session-specific cleanup operations and properly scoped within the if room_name: block.


813-816: LGTM!

The active camera initialization correctly defaults to the session's own camera and is properly placed after create_session_camera() to ensure the camera geometry exists.

src/zndraw/app/redis_keys.py (1)

345-360: LGTM!

The new session_active_camera method follows numpy-style docstrings as per coding guidelines and provides clear documentation. The Redis key pattern is hierarchical and well-structured.

app/src/hooks/useSocketManager.ts (2)

258-261: LGTM!

The handler correctly uses useAppStore.getState() pattern consistent with other handlers in this hook, and the type annotation for the data parameter is present.


732-732: LGTM!

Socket event registration and cleanup are properly paired and follow the established pattern for other event handlers in this hook.

Also applies to: 794-794

src/zndraw/session_manager.py (4)

58-76: LGTM!

The camera getter now correctly returns the geometry at the active_camera key, enabling sessions to view through any camera. The docstring accurately describes the updated behavior.


78-90: LGTM!

The camera setter correctly writes to the active_camera geometry key, consistent with the getter behavior.


143-152: LGTM!

The active_camera getter is clean and delegates to the API correctly. Type hints and numpy-style docstring are present as per coding guidelines.


154-181: LGTM!

The active_camera setter correctly validates both existence and type before updating. The validation order (existence → type) is correct, and the descriptive error messages aid debugging. The static analysis hints about long exception messages (TRY003) are acceptable here since the messages are concise and provide useful context without justifying custom exception classes.

app/src/store.tsx (4)

16-16: LGTM!

Import correctly added for getToken to support auth token retrieval for server sync.


221-221: LGTM!

New action declaration aligns with the existing pattern and matches the implementation.


1036-1067: LGTM - Good implementation of server sync.

The optimistic update pattern is correct, error handling is graceful, and auth token is properly included when available. The if (roomId && sessionId) guard appropriately prevents unnecessary requests.


1069-1072: LGTM!

Simple direct setter correctly avoids server sync loops when used by the socket event handler. The comment clearly explains the purpose.

src/zndraw/api_manager.py (1)

1743-1765: LGTM!

Implementation follows existing patterns. The method correctly delegates to the REST endpoint. Note that validation (KeyError/TypeError for invalid cameras) happens at the FrontendSession.active_camera setter level in session_manager.py, not at this REST endpoint.

tests/test_active_camera.py (8)

13-23: LGTM!

Clean, focused test validating the default active camera behavior.


26-41: LGTM!

Well-structured test for the active_camera setter.


44-62: LGTM!

Good coverage of the switch-back scenario.


65-85: LGTM!

Critical test ensuring per-session isolation of the active_camera property.


88-102: LGTM!

Good test verifying server-side persistence across API calls.


105-114: LGTM!

Proper error handling test for non-existent geometry keys.


117-129: LGTM!

Good type validation test ensuring only Camera geometries can be set as active camera.


132-153: LGTM!

Excellent integration test verifying the interaction between session.camera setter and active_camera. This ensures the camera setter updates the currently active camera rather than always the session's own camera.

src/zndraw/app/session_routes.py (2)

140-166: LGTM!

Simple and correct implementation. The endpoint returns None for the active_camera value if the Redis key doesn't exist, which is acceptable behavior for this use case.


169-217: This REST endpoint intentionally relies on boundary validation rather than endpoint-level validation.

The endpoint stores active_camera directly in Redis without validation. However, this is by design: validation happens at the call site boundaries where the endpoint is called:

  1. Frontend (store.tsx): Validates with !camera || camera.type !== "Camera" before calling the endpoint
  2. Python API (session_manager.py): Validates camera existence and type in the active_camera setter before calling the endpoint

Only authenticated clients (@require_auth) can reach this endpoint, and both trusted code paths validate before calling it. If an invalid key somehow reached Redis, accessing session.camera would raise a KeyError.

This is a deliberate trusted-client design pattern. No changes needed unless you want defense-in-depth validation at the endpoint level.

Comment thread src/zndraw/api_manager.py Outdated
Comment on lines +1716 to +1741
def get_active_camera(self, session_id: str) -> str:
"""Get active camera for a frontend session.

Parameters
----------
session_id : str
Session identifier.

Returns
-------
str
Camera geometry key this session is viewing through.

Raises
------
requests.HTTPError
If request fails.
"""
headers = self._get_headers()
response = requests.get(
f"{self.url}/api/rooms/{self.room}/sessions/{session_id}/active-camera",
headers=headers,
timeout=10.0,
)
response.raise_for_status()
return response.json().get("active_camera")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return type annotation may be inaccurate.

The method returns response.json().get("active_camera") which can be None if the key doesn't exist in Redis. The return type annotation should be str | None to accurately reflect this.

Suggested fix
-    def get_active_camera(self, session_id: str) -> str:
+    def get_active_camera(self, session_id: str) -> str | None:
         """Get active camera for a frontend session.

         Parameters
         ----------
         session_id : str
             Session identifier.

         Returns
         -------
-        str
-            Camera geometry key this session is viewing through.
+        str | None
+            Camera geometry key this session is viewing through, or None if not set.

         Raises
         ------
         requests.HTTPError
             If request fails.
         """
🤖 Prompt for AI Agents
In @src/zndraw/api_manager.py around lines 1716 - 1741, The get_active_camera
method's return type is inaccurate because response.json().get("active_camera")
may be None; update the function signature from def get_active_camera(self,
session_id: str) -> str: to return an optional type (e.g., -> str | None or ->
Optional[str]) and import Optional from typing if needed; also update the
docstring "Returns" section to indicate it can return None and ensure any
callers handle a None value appropriately.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jan 11, 2026

Codecov Report

❌ Patch coverage is 82.17054% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.97%. Comparing base (1971266) to head (a474208).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/zndraw/app/session_routes.py 24.00% 19 Missing ⚠️
src/zndraw/app/events.py 0.00% 3 Missing ⚠️
src/zndraw/app/redis_keys.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #837    +/-   ##
========================================
  Coverage   79.97%   79.97%            
========================================
  Files         162      163     +1     
  Lines       19769    19896   +127     
========================================
+ Hits        15810    15912   +102     
- Misses       3959     3984    +25     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

PythonFZ and others added 2 commits January 12, 2026 08:49
@PythonFZ PythonFZ merged commit edf0a95 into main Jan 13, 2026
6 checks passed
@PythonFZ PythonFZ deleted the feat/active-camera-per-session branch January 13, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants