Frontend → Python events#

The embedded viewer sends one of seven closed event records to Python. The browser posts JSON to POST /_cellucid/events; the server authenticates viewerId and viewerToken, removes the token, and dispatches the record to the matching viewer.

Hook entry points#

Use the event-specific decorators for interaction events:

@viewer.on_selection
def handle_selection(event):
    print(event["cells"], event["source"])

@viewer.on_hover
def handle_hover(event):
    print(event["cell"], event["position"])

@viewer.on_click
def handle_click(event):
    print(event["cell"], event["button"])

@viewer.on_ready
def handle_ready(event):
    print(event["n_cells"], event["dimensions"])

@viewer.on_message receives every accepted event in a common envelope:

@viewer.on_message
def handle_current_event(event):
    print(event["event"], event)

The envelope contains event plus the exact payload fields below. Routing fields are never exposed to hook code.

Closed event catalog#

Unknown event types and every record with missing or undeclared fields are rejected before any hook runs. Integers are native, non-negative JSON integers; booleans are native JSON booleans.

selection#

Posted record:

{
  "type": "selection",
  "viewerId": "<viewer-id>",
  "viewerToken": "<viewer-token>",
  "cells": [0, 10, 42],
  "source": "lasso"
}

Hook payload:

{"cells": [0, 10, 42], "source": "lasso"}

cells contains unique row indices. source is one exact non-empty token.

hover#

Posted record:

{
  "type": "hover",
  "viewerId": "<viewer-id>",
  "viewerToken": "<viewer-token>",
  "cell": 42,
  "position": {"x": 0.1, "y": 0.2, "z": -0.3}
}

cell and position are both null when no cell is hovered. A non-null position contains exactly finite x, y, and z numbers.

click#

Posted record:

{
  "type": "click",
  "viewerId": "<viewer-id>",
  "viewerToken": "<viewer-token>",
  "cell": 42,
  "button": 0,
  "shift": false,
  "ctrl": true
}

button is exactly 0, 1, or 2.

ready#

Posted record:

{
  "type": "ready",
  "viewerId": "<viewer-id>",
  "viewerToken": "<viewer-token>",
  "n_cells": 12345,
  "dimensions": 2
}

dimensions is exactly 1, 2, or 3.

pong#

pong is the response to the internal ping diagnostic:

{"requestId": "exact-request-id", "t": 1722000000000}

debug_snapshot#

debug_snapshot reports the iframe environment for viewer.debug_connection():

{
    "requestId": "exact-request-id",
    "ts": "2026-07-26T12:00:00.000Z",
    "locationHref": "https://viewer.example/?jupyter=true",
    "origin": "https://viewer.example",
    "serverUrl": "https://notebook.example/cellucid",
    "connected": True,
    "parentOrigin": "https://notebook.example",
    "userAgent": "Browser identification",
}

userAgent may be None; every other field is required.

session_bundle#

This record is generated by the Python upload endpoint after it has validated, stored, authenticated, and consumed one pending bundle request:

{
    "requestId": "exact-request-id",
    "status": "ok",
    "bytes": 123456,
    "path": "/temporary/path/session.cellucid-session",
}

There is no error-shaped event. An invalid upload is rejected by the HTTP endpoint and does not dispatch a hook.

Delivery and failure semantics#

  • The endpoint accepts at most 1 MiB of UTF-8 JSON.

  • The viewer ID must be registered and the token must match.

  • The event name, full field set, and every payload value are validated before hook dispatch.

  • @viewer.on_message handlers run before the event-specific handlers.

  • If any hook raises, the event is not published to viewer.state, event waiters are not notified, and the HTTP request returns a callback failure.

  • A successful hook sequence is recorded once and wakes viewer.wait_for_event(...).

Hook callbacks run on the server request thread. Keep them short and move expensive analysis out of the callback path.

Troubleshooting#

If no event arrives:

  1. Run viewer.debug_connection().

  2. Confirm server_health, frontend_roundtrip, and frontend_debug_snapshot.

  3. Check the browser network panel for POST /_cellucid/events.

  4. Confirm the notebook still owns the displayed viewer; stop stale viewers with viewer.stop().

See Troubleshooting (hooks) for the full diagnostic sequence.