Skip to content

feat(meetings): per-member meeting history with status, transcript and delete - #13

Open
Rahulkaushik01 wants to merge 1 commit into
feat/meeting-ownership-and-statusfrom
feat/meetings-history-api
Open

feat(meetings): per-member meeting history with status, transcript and delete#13
Rahulkaushik01 wants to merge 1 commit into
feat/meeting-ownership-and-statusfrom
feat/meetings-history-api

Conversation

@Rahulkaushik01

Copy link
Copy Markdown
Collaborator

Stacked PR — base feat/meeting-ownership-and-status. Merge PRs 1–5 first.
Chain: audio-engine → ingest-and-lifecycle → api → owner-and-reaper → ownership-and-status → this

Summary

The read side of per-user history: one API that lists, inspects, transcribes and deletes a
member's meetings — covering both meeting bots and local recordings, so the desktop shows a
single history regardless of how a meeting was captured. This is what makes history work across
devices instead of living in a local file on one machine.

Split by purpose rather than by session type: /local_sessions/* controls a recording while it
happens
(start, upload, pause, stop); /meetings/* is the read side (history, status,
transcript, delete). Keeping one transcript implementation behind both avoids the two paths
drifting apart.


The endpoints

GET /meetings?source=bot|local

The member's meetings, newest first, 25 per page (cursor-paginated).
Excludes deleted meetings and Zoom RTMS app sessions. An unknown source is a 400 rather
than silently returning everything.

GET /meetings/{id}

One meeting plus its status: recording · paused · finalizing · processing · done · partially_failed · deleted (via PR 5's mapping of the 19 internal states).

GET /meetings/{id}/transcript

Owner-scoped transcript for bot and local meetings alike. /local_sessions/{id}/transcript
remains as an alias onto the same code so the desktop keeps working during migration.

DELETE /meetings/{id}

Routes on state — see below.


Problems solved, and why these choices

Ownership is a filter, never a check afterwards

A meeting that isn't yours is indistinguishable from one that doesn't exist. Returning 404
(not 403) means the API never confirms another member's meeting is real.

The member id is resolved before any queryset is built

This is the one that would have been a genuine data leak. Verified behaviour: a filter built with
an empty owner id compiles to WHERE owner_user_id IS NULL.

That does not return nothing — it returns every unowned meeting in the project, i.e. all
historic bot meetings belonging to everyone. One missing value silently turns a personal query
into a company-wide one. The id is therefore resolved, and the request rejected with a 401,
before a queryset exists.

Listing costs a fixed number of queries, however long the page

BotSerializer exposes events, state, transcription_state and recording_state as method
fields, each of which hits the database per row — fine for one bot, but on a 25-row page that
is the classic N+1. This uses a lean serializer plus prefetch_related and annotated counts.

Proven, not assumed — the test asserts the query count does not grow with rows:
1 meeting → 3 queries; 25 meetings → 3 queries.

Kept in its own module because bots/serializers.py is already ~2,400 lines.

Pagination is newest-first, with a tiebreak

The existing BotCursorPagination orders by created_at ascending — verified with a live
test (meetings dated 17/18/19 July came back in that order), which would open a member's history
on their oldest meeting. This sorts by -created_at, -id. The -id matters as much as the
direction: without a unique tiebreak, meetings sharing a created_at can be skipped or
repeated
across page boundaries.

Deleted meetings are excluded

delete_data() wipes contents but keeps the row (marked deleted). Without an explicit
exclusion they resurface in history as blank entries.

Delete routes on state — three genuinely different operations

Verified: DATA_DELETED is only reachable from ENDED or FATAL_ERROR.

State Action Result
Scheduled (hasn't run) canceldelete_bot() row removed outright
Ended / fatal error delete datadelete_data() contents wiped, row kept, Redis state cleared for local sessions
Already deleted none 204 — a double-click or a retry cannot raise
Still running none 409 "stop it before deleting"

Without the first branch, deleting a scheduled meeting would answer "stop the recording first"
for something that hasn't happened. Without the third, a double-click would raise out of
delete_data() and surface as a 500. delete_bot() was deliberately not used as the
general delete: it only accepts SCHEDULED bots and removes the row entirely, which is
cancellation, not history deletion.

Reads are throttled per MEMBER, not per project

Every desktop install shares one project API key, so the existing per-project throttle would
let a single runaway client exhaust the budget for everyone. Keying on the team.day user id
confines a misbehaving client to itself. (The existing ProjectPostThrottle also returns early
for anything that isn't a POST, so these GET endpoints would otherwise have been entirely
unthrottled.)


Changes made

File Lines Purpose
bots/meetings_api_views.py (new) +166 list, detail + delete, owner-gated transcript, cursor pagination
bots/meetings_serializers.py (new) +65 lean serializer that avoids the N+1
bots/meetings_api_urls.py (new) +29 routes
bots/throttling.py +26 MemberReadThrottle — per-member read budget
bots/team_day_user_auth.py +13 quiet_user_id() for throttling (never raises)
attendee/settings/base.py +3 member_read rate (default 600/min)
attendee/urls.py +1 include the routes
version.json +1/−1 1.50.0 → 1.51.0 (minor, per feat title)

Total: 306 changed lines, 1 commit.


Testing performed

Real HTTP against a running server, with real team.day-signed tokens and real audio.

PR 6 behaviour — 31/31

  • no token / forged token / token with no user id401, never a list
  • only my meetings returned; another member's absent; newest-first ordering
  • deleted meetings excluded · Zoom app-sessions excluded · source=bot|local filters ·
    source=garbage → 400
  • every status verified end-to-end: recording, paused, processing, done,
    partially_failed
  • transcript and detail for another member → 404
  • delete while recording → 409 with a message · finished → 204, marked deleted,
    utterances gone, absent from history · second delete → 204 (no 500) · another member's →
    404 · scheduled → cancelled and the row removed
  • N+1 proof: 3 queries for 1 meeting, 3 queries for 25
  • pagination: 25 rows across pages, each appearing exactly once — no gaps, no repeats

Regression

  • test_bots_api_views, test_bots_api_utils, test_bot_data_deletion, test_cleanup
    114 tests OK
  • Full end-to-end across PRs 1→6 → 18/18 with real audio: start → owner stamped → other
    member blocked → heartbeat → pause → status paused → resume → 8 chunks → appears in history
    → delete-while-recording 409 → stop → ENDED → recording COMPLETE → real transcription
    → transcript via /meetings → delete → gone from history

Measured latency at production pacing (1 chunk/second, a full 94-second recording):

Sentence finished → text available avg 4.1s (2.4 – 7.0s)
Lines appearing while still recording 8 of 9
Finalize after Stop 0.6s
Everything transcribed after Stop 3.1s
Audio captured in utterances 96% (remainder is genuine silence)

Gates: ruff check ✅ · ruff format --check ✅ · manage.py check ✅ ·
version.json bump simulated against the CI rule ✅


Coding-guidelines compliance

  • File size (§2): new files are 166 / 65 / 29 — all under the 250 preferred level. The lean
    serializer was put in its own module specifically to avoid growing bots/serializers.py
    (~2,400 lines) further. ⚠️ attendee/settings/base.py is now 349 lines (+3 here) — under the
    350 hard limit but over the 300 warning; splitting settings by concern would be a small,
    separate PR.
  • Functions (§6): longest is 34 lines; all single-responsibility with early returns.
  • Constants (§5): SOURCE_TO_SESSION_TYPE, SESSION_TYPE_TO_SOURCE, page size and throttle
    scope are named; no magic values. The throttle rate is env-overridable.
  • Error handling (§12): nothing fails silently — 400 / 401 / 404 / 409 with explicit
    messages, and every path that could raise out of the model layer is guarded first.
  • Security (§8): no secrets added; ownership is only ever taken from a verified token;
    mismatches return 404 so existence never leaks; reads are rate-limited per member.
  • Testing (§10): unit, integration, permission, error-state and performance (query-count)
    coverage.
  • Commits (§14): type(scope): description.
  • No dead code, debug logs, or unused imports (§15).

Risks / notes

  • Bot history will be empty at first — this is expected, not a bug. We chose a fresh start
    (no backfill), so a bot meeting only gains an owner when the desktop sends the user token on
    dispatch
    , which is not built yet. Local recordings appear immediately. Anyone testing this PR
    in isolation will see an empty source=bot list.
  • MeetingTranscriptView subclasses the existing TranscriptView, adding the ownership gate
    in front. Duplicating the transcript logic was rejected as guaranteed drift; the trade-off is a
    dependency on that view's internals.
  • The transcript endpoint returns every utterance with no paging — fine at current sizes, but
    a very long meeting would return a large payload. Worth a limit later.
  • version.json is 1.51.0, one minor above this PR's base. If the earlier PRs' versions are
    bumped before this merges, this one needs re-bumping to stay one minor above its base.
  • Per-member privacy is only as strong as the token it trusts — see the separate note on the
    shared JWT signing secret.

…d delete

One history covering both meeting bots and local recordings, scoped to the member
named by the verified X-User-Token.

* GET /meetings lists a member's meetings newest-first. Deleted meetings are
  excluded (delete_data keeps the row, so they would otherwise resurface as blank
  entries) and so are Zoom RTMS app sessions, which are neither a bot nor a local
  recording. An unknown ?source= is a 400 rather than silently returning
  everything.

* Ownership is applied as a FILTER, never a check afterwards, so a meeting that
  isn't yours is indistinguishable from one that doesn't exist -- a 404 never
  confirms it is real. The member id is resolved before any queryset is built,
  because filter(owner_user_id=None) compiles to IS NULL and would return every
  unowned meeting in the project.

* Listing costs a fixed number of queries however long the page is. BotSerializer
  exposes events/state/transcription_state as method fields that each hit the
  database per row, which turns one page into dozens of queries; this uses a lean
  serializer plus prefetch and annotated counts instead. Proven by test: 1 meeting
  and 25 meetings both take 3 queries.

* Pagination is newest-first with the row id as a tiebreak. The existing
  BotCursorPagination sorts ascending, which would open history on the oldest
  meeting, and without a unique tiebreak rows sharing a created_at can be skipped
  or repeated across page boundaries.

* DELETE routes on state: a meeting that hasn't run yet is CANCELLED (the row is
  removed), a finished one has its data deleted (row kept, contents wiped, Redis
  state cleared for local sessions), an already-deleted one returns 204 so a
  double-click or a retry cannot raise, and anything still running returns 409
  telling the member to stop it first.

* Reads are throttled per MEMBER, not per project: every desktop install shares
  one project API key, so a per-project limit would let one runaway client exhaust
  the budget for everyone.

Co-Authored-By: Claude <noreply@anthropic.com>
@Rahulkaushik01
Rahulkaushik01 requested a review from hd1801 July 21, 2026 08:24
@Rahulkaushik01
Rahulkaushik01 changed the base branch from feat/meeting-ownership-and-status to dev July 22, 2026 04:41
@Rahulkaushik01
Rahulkaushik01 changed the base branch from dev to feat/meeting-ownership-and-status July 22, 2026 04:44
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.

1 participant