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