fix(voice): borrow upstream call fixes into the CE voice stack and restore the Calls page - #52
Conversation
Contact merge reassigned conversations, messages, notes and contact inboxes but left calls pointing at the contact that is about to be destroyed. calls.contact_id has no foreign key, so those rows survived as orphans and the call list rendered them without a contact. Move the mergee's calls onto the base contact inside the same transaction, and declare has_many :calls on Contact so a plain destroy cleans them up the way it already does for every other child record.
The calls table, Call#push_event_data and the VoiceCall bubble all already carry a transcript, but the webhook controller never let one through: update_params did not permit :transcript and neither of the apply paths wrote it, so the field stayed nil for every call. Permit it and treat it like recording_url — a post-call artifact that necessarily arrives after the terminal status, and therefore one of the fields a stale-status webhook may still carry. persist already touches the linked message, so the bubble re-renders with the transcribed text without any frontend change.
MediaRecorder streams every Cluster with an unknown size, and the EBML walker descends into those inline rather than recursing so a long recording does not cost one stack frame per cluster. Nothing guarded that: reinstating the recursive descent still produced a byte-identical file for short inputs. Build a WebM the way MediaRecorder shapes one — an unknown-size Segment holding a long run of unknown-size Clusters — and assert every frame reaches the OGG output, which overflows the stack without the inline descent.
The join endpoint records accepted_by_agent_id and touches the linked message, so every dashboard already receives who answered. The bubble ignored it and kept offering Join, sending late clickers into a 409 from the Pathors relay. Gate the button on the field instead of clearing it on leave. Nothing clears accepted_by_agent_id today, and nothing should: it is the persisted attribution the bubble's "Handled by" line and the calls list's "Picked by" column read, and leave is a purely client-side LiveKit disconnect with no request a crashed or closed tab could be relied on to send. Comparing the field against the current user keeps the call closed to everyone else while letting the agent who left rejoin their own call.
The sidebar's Calls page fetches GET /api/v1/accounts/:id/calls, but that route only ever existed behind the enterprise guard, so the page 404d on every load in this fork. Add the community endpoint: a CallFinder that scopes by account, restricts non-administrators to the conversations they can open (the same bar the Pathors join endpoint enforces), applies the status, direction, inbox and agent filters the filter bar sends, and paginates 25 to a page. Call gains the inverse of its display mappings so the dashboard's inbound/outbound and dashed statuses round-trip. The serializer keeps contact and agent nullable — an AI-handled call has no agent, and a contact deleted after the call would otherwise drop its history — and zh_TW picks up the deleted-contact fallback string the list already renders.
The Calls entry and route were gated on Chatwoot Cloud / Enterprise because the history endpoint only existed there. The fork now serves it from its own CE controller, so the only gate left is the channel_voice feature flag.
ReviewSolid PR overall — the new endpoint reuses 1. The enterprise
|
|
@yui0303 AI check 沒問題(rubocop / rspec 118 / eslint / vitest),可以 review/merge。無 migration。跟 voice 相關:Join 按鈕改成比對 acceptedByAgentId(不清欄位,讓同一個人離開後能再加入),Calls 頁入口改依 channel_voice 旗標。 |
Four behaviours upstream fixed for their Twilio / WhatsApp calling are re-implemented for the fork's Pathors-based CE voice, and the Calls page, which has pointed at an endpoint that did not exist since the EE voice stack was removed, works again.
Refs pathorsAI/pathors#2780 · proposal: https://claude.ai/code/artifact/26679670-e46d-47d9-8eec-54433e0e98ee (section 06)
How to test
PATCH …/pathors/calls/:idwithtranscript): the voice bubble shows the transcript without a page reload.What changed
ContactMergeAction#merge_callsmoves calls to the base contact;Contact has_many :calls, dependent: :destroy_async.transcriptthrough the same post-terminal path asrecording_url.webmOpusToOgg: the parser fix itself already arrived with the upstream sync; this adds the missing regression spec.VoiceCall.vue: Join is gated onacceptedByAgentIdbeing empty or the current user (the field is the persisted "handled by" attribution, so it is compared, not cleared).Api::V1::Accounts::CallsController#index+CallFinder(admins see the account, agents see calls in conversations they can open),_call.json.jbuilder, route outside every enterprise guard; Sidebar andcalls/routes.jsgate the page onchannel_voiceinstead of Cloud/Enterprise. zh_TW string for the deleted-contact label.No migrations.