Zoom Keyhole

Documentation Status Pipeline Status Coverage Report

A small FastAPI service that shows who is in a Zoom room on one or more Miro boards. Room configuration (users, rooms, boards, role symbols) is read from a Google Sheet; Miro text widgets are updated to display live participants. The service listens for Zoom webhooks, periodically scans Miro boards, and updates text widgets to reflect live meeting state.

Requirements

  • Python 3.10–3.12 with Poetry

  • Make (to use the provided targets)

  • Optional: Docker (for local container runs) and Helm (for Kubernetes)

Local development

  1. Clone and install dependencies using Poetry:

poetry install
  1. Configure environment (recommended via .env in the project root). See Environment variables below for details. The Makefile auto-loads .env.

  2. Activate the virtualenv when running commands manually:

poetry shell

Quick start

  1. Prepare your Miro boards:

  • Add a text widget per Zoom room, with the room name and the literal text “Meeting ID:”. The service will insert the meeting ID as a clickable link and keep the participant list up to date.

  1. Prepare your Google Sheet:

  • Create a sheet with tabs named: Users, Rooms, Boards, Role symbols. Populate them according to your conventions. The service periodically reloads these tabs.

  1. Run locally:

  • Create a .env file (or export the variables in your shell) with the required environment variables listed below. The Makefile will auto-load .env.

  • Start the service:

make run
# or:
uvicorn src.zoom_keyhole.main:APP --host 0.0.0.0 --port 8000 --loop asyncio --reload

The service exposes its routes under the optional ROOT_PATH (default is set by the Helm chart).

Run locally

# Using the Makefile (auto-reload)
poetry run make run

# Or directly with uvicorn
poetry run uvicorn src.zoom_keyhole.main:APP \
  --host 0.0.0.0 --port 8000 --loop asyncio --reload

Once running, the service listens on http://0.0.0.0:8000 (or the port you choose). If deploying behind a reverse proxy, set ROOT_PATH accordingly.

Run with Docker

# Build a local image with an empty ROOT_PATH (good for localhost)
make docker-build-local

# Run the container, passing variables from .env if present
make run-docker PORT=8000

Linting

poetry run make python-lint

This runs isort, black (check), flake8, and pylint. Reports are written under build/reports/ where applicable.

Testing

poetry run make python-test

This runs the pytest suite with coverage. Outputs include JUnit XML and coverage HTML in build/reports/code-coverage/index.html.

Environment variables

Required:

  • ZOOM_KEYHOLE_MIRO_API_TOKEN: Miro API token. For multiple tokens, use a semicolon-separated list locally; when set via Helm/values, use colon-separated.

  • ZOOM_KEYHOLE_ZOOM_CLIENT_ID: Zoom OAuth client ID.

  • ZOOM_KEYHOLE_ZOOM_CLIENT_SECRET: Zoom OAuth client secret.

  • ZOOM_KEYHOLE_ZOOM_ACCOUNT_ID: Zoom account ID for server-to-server OAuth.

  • ZOOM_KEYHOLE_WEBHOOK_SECRET_TOKEN: Secret used to validate Zoom webhook URL challenges and payloads.

  • ZOOM_KEYHOLE_GSHEET_ID: Google Sheet ID containing configuration.

  • One of:

    • GOOGLE_SERVICE_ACCOUNT_KEY_B64: Base64-encoded Google service account JSON (recommended), or

    • ZOOM_KEYHOLE_GSHEET_KEY: Google Sheets API key (fallback).

Optional (sensible defaults exist):

  • ROOT_PATH: URL prefix when running behind a reverse proxy (Helm defaults to /ska/zoom-keyhole).

  • ENABLE_DEBUG_METHODS: Enable debug/admin routes (default: true). Disable in production.

  • ZOOM_KEYHOLE_DEBUG_TOKEN: Optional token required to access debug endpoints when set. If provided, clients must present either Authorization: Bearer <token> or X-Debug-Token: <token>.

  • ZOOM_KEYHOLE_FILE_LOGGER: Enable file logging when set to a non-zero value.

  • Scan/refresh periods (seconds): ZOOM_ROOM_SCAN_PERIOD, MIRO_BOARD_SCAN_PERIOD, MIRO_BOARD_UPDATE_PERIOD, GSHEET_USERS_SCAN_PERIOD, GSHEET_BOARDS_SCAN_PERIOD, GSHEET_ROOMS_SCAN_PERIOD, GSHEET_ROLES_SCAN_PERIOD.

Project layout

src/zoom_keyhole/           FastAPI app and domain logic
  main.py                   Uvicorn/ASGI entrypoint
  lifespan.py               App startup/shutdown lifecycle
  routes/                   API + debug endpoints (webhook, metrics, etc.)
  gsheet/                   Google Sheets load/write helpers
  miro_*                    Miro discovery/update utilities
  models/                   Pydantic models and data shapes
tests/                      Pytest suite and examples
charts/                     Helm chart for Kubernetes deployment
docs/                       Sphinx documentation sources
Makefile                    Dev shortcuts (run, lint, test, docker helpers)

Kubernetes (Helm)

The included chart deploys the service and sets the default ingress path to /ska/zoom-keyhole. Install by providing sensitive values at install time:

helm install zoom ./charts/ska-zoom-keyhole --values values.yaml

Minimal values.yaml example (redacted):

---
env:
  ZOOM_KEYHOLE_MIRO_API_TOKEN: "<token or token:token2>"
  ZOOM_KEYHOLE_ZOOM_CLIENT_ID: "<client id>"
  ZOOM_KEYHOLE_ZOOM_CLIENT_SECRET: "<client secret>"
  ZOOM_KEYHOLE_ZOOM_ACCOUNT_ID: "<account id>"
  ZOOM_KEYHOLE_WEBHOOK_SECRET_TOKEN: "<webhook secret>"
  ZOOM_KEYHOLE_GSHEET_ID: "<sheet id>"
  # Choose one auth method for Google Sheets:
  GOOGLE_SERVICE_ACCOUNT_KEY_B64: "<base64 service account json>"
  # or, as a fallback:
  ZOOM_KEYHOLE_GSHEET_KEY: "<api key>"
  # Optional
  ROOT_PATH: "/ska/zoom-keyhole"

In environments using an ingress controller (e.g. NGINX), the default path is /ska/zoom-keyhole. Adjust ROOT_PATH and ingress settings as needed in the chart values.

Debug endpoints

Debug/admin endpoints are mounted under the /debug prefix when ENABLE_DEBUG_METHODS=true (default for local dev). In production, set ENABLE_DEBUG_METHODS=false to completely disable these endpoints (404). Optionally, set ZOOM_KEYHOLE_DEBUG_TOKEN to require a token for access even in non-prod.

  • Enable and protect debug endpoints:

export ENABLE_DEBUG_METHODS=true
export ZOOM_KEYHOLE_DEBUG_TOKEN=supersecret
  • Example calls:

# List config keys
curl -H "Authorization: Bearer supersecret" \
  http://localhost:8000/debug/config/keys

# View users config
curl -H "X-Debug-Token: supersecret" \
  http://localhost:8000/debug/users/config

# Reset Zoom rooms config
curl -X POST -H "Authorization: Bearer supersecret" \
  http://localhost:8000/debug/zoom_rooms/config/reset

Note: Some debug actions can be disruptive (e.g., forcing scans/updates). Use them with care and avoid exposing them publicly.

Pagination (limit/offset) for heavy GETs:

  • The following endpoints accept limit and offset query params to constrain payload size:

    • /debug/users/config → slices the users.data array.

    • /debug/zoom_data/config → paginates rooms (optionally filtered with only_with_keyholes=true).

    • /debug/directories/config → paginates the directories list (when present).

    • /debug/zoom_rooms → paginates the zoom_rooms.data mapping by room name.

Examples:

# First 25 users
curl -H "Authorization: Bearer supersecret" \
  'http://localhost:8000/debug/users/config?limit=25&offset=0'

# Next page of Zoom data, filtered to rooms with keyholes
curl -H "Authorization: Bearer supersecret" \
  'http://localhost:8000/debug/zoom_data/config?only_with_keyholes=true&limit=10&offset=10'

# Zoom rooms page
curl -H "Authorization: Bearer supersecret" \
  'http://localhost:8000/debug/zoom_rooms?limit=20&offset=0'

Security tips

  • Do not commit secrets. .env is ignored and intended for local use.

  • Prefer GOOGLE_SERVICE_ACCOUNT_KEY_B64 for Sheets authentication (base64-encoded service account JSON). Locally, multiple Miro tokens can be ;-separated; in Kubernetes/Helm values use :-separated tokens.

Documentation

Full documentation (architecture, configuration, operations) is available on Read the Docs:

https://zoom-keyhole.readthedocs.io/en/latest/

For chart defaults, see charts/ska-zoom-keyhole/values.yaml in this repository.

CI Version Check

  • The pipeline includes a pre-lint “verify” stage that checks version consistency across code, docs, Helm chart, and Dockerfile.

  • It runs scripts/check_versions.py and fails the pipeline if mismatches are found.

  • Run locally with: make check-version.

Disabled Miro boards

  • Boards are declared in the Google Sheet (Boards tab) with name, id, and enabled.

  • When a board’s enabled is set to false in configuration:

    • All discovered widgets on that board are marked in-place with “(Board updates disabled)”.

    • Widgets are locked (editable: false) so their content is clearly frozen.

    • The service does not update those widgets further while the board remains disabled.

  • Re-enable the board by setting enabled to true; normal updates resume automatically on the next cycle.