componentsfix(meetings): a Circleback recording that plays, on open, with a link that goes to Circleback
Three things stood between a Circleback meeting and its recording.
The recording could not be fetched at all. credentials-encryption derives its
AES key from ORG_CREDENTIALS_ENCRYPTION_KEY || BETTER_AUTH_SECRET, so which
secret a credential was written under is a property of the environment at WRITE
time. Every Circleback MCP token set was written under BETTER_AUTH_SECRET; once
the org key was set, every decrypt died with "Cipher job failed", the call sites
swallowed it into "no credentials", and the player said "No playable recording
for this meeting" with the real cause visible nowhere. Decryption now tries each
configured secret; encryption still writes with the first.
The meeting dialog waited for a press before fetching. That poster exists so a
coaching document with ten moments does not fire ten mints on open against a
Fathom budget of 30/min — an argument that does not apply to one recording the
reader opened to watch. `autoLoad` skips the press without autoplaying.
"Open in Circleback" went to storage.googleapis.com. What ingest stored is a
signed GCS object whose signature dies 24h later; unsigned it answers 403
forever. The page is built from the meeting id instead, and no surface will link
to a raw media file any more.
Webhook-era rows (numeric external_id, all before 2026-07-29) still cannot mint
or link — neither ReadMeetings nor the page route takes a numeric id.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 5, 2026, 12:21 PMhooksfeat(canvas): remove sharing — canvases are single-owner
Two mechanisms are gone:
Per-person grants (canvas_access + shareCanvas/revokeAccess/claimEmailShares).
The procedures existed but nothing in the UI ever called them, so the table never
held a row.
Org-level visibility ('org'/'managers' + getTeamCanvases). The panel behind it was
never rendered either, but this one was reachable: 9 "Org Pipeline" canvases were
marked 'managers' and two users had opened a colleague's.
Access is now ownership. assertCanvasAccess drops from three branches to one, and
getCanvases from a DISTINCT ON over three visibility paths with two LEFT JOINs to a
plain owner-scoped select. canvas_user_settings goes too — it only ever stored a
HomeView position for a canvas someone else owned.
An "Org Pipeline" canvas is still org-WIDE in the data it spans
(view_config.ownerUserIds = 'all_org'); what changes is that each person reads their
own copy. The two users who could see a colleague's canvas but owned no copy —
<email> and <email> — were given one before this landed,
while `visibility` could still identify which canvases were team-visible.
MIGRATION IS DEPLOY-ORDERED. drop_canvas_sharing.sql must run AFTER this deploys:
the currently-running code selects and inserts canvases.visibility, so dropping the
column first breaks canvas listing and creation for everyone.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 9, 2026, 4:54 PMslicefix(pipeline,inbox): filters that survive a refresh, and a lost deal that leaves the board
Three reported bugs, two of them the same mechanism seen from different ends.
A rejected viewConfig save was invisible. Every filter control fires
`void saveCanvasViewConfig(id)`, so a rejection had no observer at all: no toast,
no retry, and a local config the user believed was stored. `dirtyCanvasIds` kept
the edit on screen — but that Set was never persisted while `canvasesById` was,
so a reload came back with the change still in localStorage and nothing marking
it unsaved. The server's pre-save row then won rehydration, which is why this
read as "my pipeline filters don't survive a refresh" rather than as a write that
failed. The save now reports failure and resolves false instead of rejecting, so
all ~20 `void` call sites are covered without touching them; the dirty set
persists, so the guard already in upsertCanvas holds across the reload it was
being thrown away by; and CanvasHeader stops flashing "Saved" over a failure.
Saves are also chained per canvas. Working through a filter popover fired several
updateCanvas mutations at one row concurrently, so the last RESPONSE to land, not
the last edit made, decided what the server kept.
Closed Lost deals stayed in the pipeline because status is filtered SERVER-side:
a page's membership is fixed when it is fetched, and marking a deal Closed Lost
writes to the store, never to that page. The status/priority change now
invalidates listConversations — it was the only successful-update path that never
did — and the fetched rows are re-checked against the optimistic value, so the row
leaves in the same frame as the click and the refetch merely agrees. That check is
one shared predicate now; the kanban had its own copy, which is why two views of
one pipeline disagreed about what a stage filter admits.
Escape clears the inbox filters, in a second stage. Selection first, filters only
once nothing is selected — a single stage would throw a filter set away on every
ordinary deselect.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 4:06 PMtypesfix(mail): stop a list load selecting a deal, and rename the Status label to Stage
A fresh /agent chat came up bound to whatever deal sorted first. setCurrentConversationList
auto-selected conversations[0] into activeConversationId, which on the chat surfaces is not a
row highlight but the CHAT'S CONTEXT — useRouteChatThread writes it onto the thread's
selectedArtifact and the attach effect commits it as primaryConversation. So the Top Deals tile
finishing its load put a deal nobody opened into a brand-new chat's chip row and into what the
agent was told. Loading a list now selects nothing; j/k is unaffected (focusedIndex starts null,
so the first press still resolves to row 0 — and no longer skips it).
CardListCanvasView also claimed the global currentConversationList and read it back to render.
useCanvasConversations gains syncCurrentList and returns the ordered list, so a tile that merely
displays deals no longer repoints the app's shared selection state. That leaves nothing writing
that list on /agent.
Rename the deal status field's LABEL to "Stage" everywhere it shows (the filter builder, table
header, group-by, kanban, profile, onboarding preview). Reported on Vooma's onboarding: "these
are stages, not statuses — status is like open and closed." Column id stays `status`;
statusOverview keeps its name, being a prose summary rather than the stage enum.
Add mod+N for a new chat, sharing one implementation with the chat header's button so the two
cannot drift, plus a row in the hotkey bar. The desktop File menu had CmdOrCtrl+N on Compose,
which the main process consumes before the page sees it — moved to CmdOrCtrl+Shift+N. A plain
browser tab reserves the key and will never dispatch it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 17, 2026, 11:06 PMutilsfix(canvas): stop a stale getCanvases cache from reverting saved view configs
A canvas viewConfig save wrote to the server but left the pre-save row in the
cached canvas.getCanvases response. The store rehydrates from that query on
every consumer mount, and it is persisted to IndexedDB, so remounting or
reloading inside the staleTime window put the old config back — and the next
save then wrote that stale config to the server, losing the change for good.
The existing dirty-flag guard only covers the window while a save is in flight.
Saves now patch the cached rows, and upsertCanvas rejects any snapshot older
than the one it holds, which also closes the in-flight-refetch race.
Also default new kanban boards to CRM-synced deals only when a CRM is
connected: the default existed for the list canvas, but the kanban branch never
had it and /pipeline's create path called buildCanvasViewConfig without the
hasCrmConnected option, so nothing created there got it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 4, 2026, 3:47 PM