v0.6.31: elevenlabs voice, trigger.dev fixes, cloud whitelabeling for enterprises#4053
v0.6.31: elevenlabs voice, trigger.dev fixes, cloud whitelabeling for enterprises#4053waleedlatif1 merged 9 commits intomainfrom
Conversation
waleedlatif1
commented
Apr 8, 2026
- feat(voice): voice input migration to eleven labs (feat(voice): voice input migration to eleven labs #4041)
- fix(kb): doc selector (fix(kb): doc selector #4048)
- fix(kb): disable connectors after repeated sync failures (fix(kb): disable connectors after repeated sync failures #4046)
- fix(parallel): remove broken node-counting completion + resolver claim cross-block (fix(parallel): remove broken node-counting completion + resolver claim cross-block #4045)
- debug(log): Add logging on socket token error (debug(log): Add logging on socket token error #4051)
- fix(trigger): add react-dom and react-email to additionalPackages (fix(trigger): add react-dom and react-email to additionalPackages #4052)
- fix(webhook): throw webhook errors as 4xxs (fix(webhook): throw webhook errors as 4xxs #4050)
- feat(enterprise): cloud whitelabeling for enterprise orgs (feat(enterprise): cloud whitelabeling for enterprise orgs #4047)
* feat(speech): unified voice interface * add metering for voice input usage * ip key * use shared getclientip helper, fix deployed chat * cleanup code * prep merge * merge staging in * add billing check * add voice input section * remove skip billing * address comments
* fix(kb): improve error logging when connector token resolution fails The generic "Failed to obtain access token" error hid the actual root cause. Now logs credentialId, userId, authMode, and provider to help diagnose token refresh failures in trigger.dev. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(kb): disable connectors after 10 consecutive sync failures Connectors that fail 10 times in a row are set to 'disabled' status, stopping the cron from scheduling further syncs. The UI shows an alert triangle with a reconnect banner. Users can re-enable via the play button or by reconnecting their account, which resets failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(kb): disable sync button for disabled connectors, use amber badge variant Sync button should be disabled when connector is in disabled state to guide users toward reconnecting first. Badge variant changed from red to amber to match the warning banner styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(kb): address PR review comments for disabled connector feature - Use `=== undefined` instead of falsy check for nextSyncAt to preserve explicit null (manual sync only) when syncIntervalMinutes is 0 - Gate Reconnect button on serviceId/providerId so it only renders for OAuth connectors; show appropriate copy for API key connectors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(kb): move resolveAccessToken inside try/catch for circuit-breaker coverage Token resolution failures (e.g. revoked OAuth tokens) were thrown before the try/catch block, bypassing consecutiveFailures tracking entirely. Also removes dead `if (refreshed)` guards at mid-sync refresh sites since resolveAccessToken now always returns a string or throws. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(kb): remove dead interval branch when re-enabling connector When `updates.nextSyncAt === undefined`, syncIntervalMinutes was not in the request, so `parsed.data.syncIntervalMinutes` is always undefined. Simplify to just schedule an immediate sync — the sync engine sets the proper nextSyncAt based on the connector's DB interval after completion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…m cross-block (#4045) * fix(parallel): remove broken node-counting completion in parallel blocks * fix resolver claim --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Theodore Li <theo@sim.ai>
* fix(webhook): throw webhook errors as 4xxs * Fix shadowing body var --------- Co-authored-by: Theodore Li <theo@sim.ai>
* feat(enterprise): cloud whitelabeling for enterprise orgs * fix(enterprise): scope enterprise plan check to target org in whitelabel PUT * fix(enterprise): use isOrganizationOnEnterprisePlan for org-scoped enterprise check * fix(enterprise): allow clearing whitelabel fields and guard against empty update result * fix(enterprise): remove webp from logo accept attribute to match upload hook validation * improvement(billing): use isBillingEnabled instead of isProd for plan gate bypasses * fix(enterprise): show whitelabeling nav item when billing is enabled on non-hosted environments * fix(enterprise): accept relative paths for logoUrl since upload API returns /api/files/serve/ paths * fix(whitelabeling): prevent logo flash on refresh by hiding logo while branding loads * fix(whitelabeling): wire hover color through CSS token on tertiary buttons * fix(whitelabeling): show sim logo by default, only replace when org logo loads * fix(whitelabeling): cache org logo url in localstorage to eliminate flash on repeat visits * feat(whitelabeling): add wordmark support with drag/drop upload * updated turbo * fix(whitelabeling): defer localstorage read to effect to prevent hydration mismatch * fix(whitelabeling): use layout effect for cache read to eliminate logo flash before paint * fix(whitelabeling): cache theme css to eliminate color flash before org settings resolve * fix(whitelabeling): deduplicate HEX_COLOR_REGEX into lib/branding and remove mutation from useCallback deps * fix(whitelabeling): use cookie-based SSR cache to eliminate brand flash on all page loads * fix(whitelabeling): use !orgSettings condition to fix SSR brand cache injection React Query returns isLoading: false with data: undefined during SSR, so the previous brandingLoading condition was always false on the server — initialCache was never injected into brandConfig. Changing to !orgSettings correctly applies the cookie cache both during SSR and while the client-side query loads, eliminating the logo flash on hard refresh.
PR SummaryHigh Risk Overview Introduces enterprise org whitelabeling backed by a new Fixes and hardens several platform behaviors: knowledge connectors can become Reviewed by Cursor Bugbot for commit 1189400. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1189400. Configure here.
Greptile SummaryThis release bundles eight changes: ElevenLabs WebSocket-based STT migration, cloud whitelabeling for enterprise orgs (new DB column, API, settings UI, theme injection), parallel-executor completion fix, webhook 4xx error propagation, KB connector disable-on-failure, and trigger.dev package additions.
Confidence Score: 3/5Mergeable with caution — two P1 correctness issues should be addressed before or shortly after shipping. The parallel and webhook fixes are clean and well-tested. The whitelabeling API and UI are well-guarded. However, the STT billing-before-permission ordering means users who deny mic access are charged for sessions that never happen, and the 3-digit hex bug in inject-theme.ts can silently produce wrong button text colors for env-var-configured brand colors. apps/sim/app/api/speech/token/route.ts and apps/sim/hooks/use-speech-to-text.ts (billing ordering), apps/sim/ee/whitelabeling/inject-theme.ts (3-digit hex handling)
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/speech/token/route.ts | New ElevenLabs STT token endpoint with rate limiting and billing — records usage before mic permission is confirmed on the client, causing potential over-billing on mic denial. |
| apps/sim/hooks/use-speech-to-text.ts | Migrates STT from browser Web Speech API to ElevenLabs WebSocket streaming; uses deprecated createScriptProcessor and fetches token (triggering billing) before mic permission is obtained. |
| apps/sim/ee/whitelabeling/inject-theme.ts | Generates CSS variable overrides from env-var brand config; isDarkBackground does not expand 3-digit hex colors before parsing, producing wrong luminance and incorrect contrast text color. |
| apps/sim/app/api/organizations/[id]/whitelabel/route.ts | Well-structured GET/PUT endpoint for org whitelabel settings; validates membership, role (owner/admin), and enterprise plan before allowing mutations. |
| apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx | New settings UI for org whitelabeling; initializes form state in the render body (not useEffect) which is an anti-pattern but non-critical; otherwise well-structured with proper billing guards. |
| apps/sim/executor/orchestrators/parallel.ts | Removes broken node-count-based completion detection in favour of sentinel-end-node edge mechanism; simplifies aggregation and fixes cross-block resolver claims. |
| apps/sim/lib/webhooks/providers/generic.ts | Adds custom success response formatting (status code + body) for generic webhook provider; correctly validates status code range. |
| apps/sim/ee/whitelabeling/hooks/whitelabel.ts | React Query hooks for fetching/updating whitelabel settings; follows project conventions with explicit staleTime, signal forwarding, and onSettled for cache invalidation. |
| apps/sim/app/_styles/globals.css | Adds default CSS variable values for auth button colors needed by the whitelabeling theme system; modifying globals.css violates project convention but is technically necessary for root-level CSS defaults. |
| packages/db/schema.ts | Adds whitelabelSettings JSONB column to the organization table for storing per-org branding configuration. |
Sequence Diagram
sequenceDiagram
participant U as User
participant C as Client (useSpeechToText)
participant ST as /api/speech/token
participant EL as ElevenLabs API
participant B as Billing
U->>C: toggleListening()
C->>ST: POST /api/speech/token
ST->>EL: POST /v1/single-use-token/realtime_scribe
EL-->>ST: { token }
ST->>B: recordUsage (full session cost)
ST-->>C: { token }
C->>C: getUserMedia() — mic permission dialog
alt Mic Granted
C->>EL: WebSocket connect (token)
EL-->>C: partial_transcript / committed_transcript
C->>U: onTranscript(text)
else Mic Denied
C->>C: cleanup() — token and billing already consumed
note right of C: User billed, no STT used
end
Comments Outside Diff (2)
-
apps/sim/ee/whitelabeling/inject-theme.ts, line 6-9 (link)3-digit hex colors produce incorrect luminance and wrong button text color
isDarkBackgroundin this file assumes a 6-digit hex string. For a 3-digit value like#abc, parsing gives wrong channel values (r=0xab,g=0xc,b=NaN), so the luminance check is unreliable and the auto-contrasted button text color may be incorrect.org-branding-utils.ts(the equivalent function used for DB-sourced settings) correctly expands 3-digit hex before parsing.HEX_COLOR_REGEX(used for validation) explicitly allows 3-digit values, so an environment variable like#fffis accepted but handled incorrectly here. Additionally,substris deprecated —org-branding-utils.tsalready usessliceconsistently. -
apps/sim/app/_styles/globals.css, line 18-23 (link)Global stylesheet modified for whitelabel CSS variables
Per project conventions, edits to
globals.cssshould be avoided in favour of local component styles. The new--auth-primary-btn-*CSS variables are used as default fallbacks for the whitelabeling theme system and are overridden byinject-theme.ts/generateOrgThemeCSS. Since these are true:root-level defaults that must be in place before any component renders, theglobals.cssplacement is justifiable — but it's worth documenting the necessity in a comment so future contributors know not to remove them.Rule Used: Avoid editing the globals.css file unless absolute... (source)
Learnt From
simstudioai/sim#367Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (1): Last reviewed commit: "feat(enterprise): cloud whitelabeling fo..." | Re-trigger Greptile
| if (billingUserId) { | ||
| const maxMinutes = chatId ? CHAT_SESSION_MAX_MINUTES : WORKSPACE_SESSION_MAX_MINUTES | ||
| const sessionCost = VOICE_SESSION_COST_PER_MIN * maxMinutes | ||
|
|
||
| await recordUsage({ | ||
| userId: billingUserId, | ||
| entries: [ | ||
| { | ||
| category: 'fixed', | ||
| source: 'voice-input', | ||
| description: `Voice input session (${maxMinutes} min)`, | ||
| cost: sessionCost * getCostMultiplier(), | ||
| }, | ||
| ], | ||
| }).catch((err) => { | ||
| logger.warn('Failed to record voice input usage, continuing:', err) | ||
| }) | ||
| } | ||
|
|
||
| return NextResponse.json({ token: data.token }) |
There was a problem hiding this comment.
Usage billed before microphone permission is confirmed
The server records usage (line 150) and issues the ElevenLabs token before the client has checked or obtained microphone access. In use-speech-to-text.ts, getUserMedia is called only after the token response is received. If the user denies microphone permission, NotAllowedError is thrown, cleanup runs, and the session is abandoned — but billing was already recorded and the single-use token was consumed.
Consider requesting getUserMedia on the client before calling /api/speech/token, or deferring usage recording to after the WebSocket connection is successfully established.
| const processor = audioContext.createScriptProcessor(4096, 1, 1) | ||
| processorRef.current = processor |
There was a problem hiding this comment.
createScriptProcessor is deprecated
AudioContext.createScriptProcessor() was deprecated in the Web Audio API spec in favour of AudioWorklet. It still functions in most browsers but emits deprecation warnings and is expected to be removed eventually. Migrating to an AudioWorkletProcessor would future-proof this and also move audio processing off the main thread.
| if (savedSettings && !formInitialized) { | ||
| setBrandName(savedSettings.brandName ?? '') | ||
| setPrimaryColor(savedSettings.primaryColor ?? '') | ||
| setPrimaryHoverColor(savedSettings.primaryHoverColor ?? '') | ||
| setAccentColor(savedSettings.accentColor ?? '') | ||
| setAccentHoverColor(savedSettings.accentHoverColor ?? '') | ||
| setSupportEmail(savedSettings.supportEmail ?? '') | ||
| setDocumentationUrl(savedSettings.documentationUrl ?? '') | ||
| setTermsUrl(savedSettings.termsUrl ?? '') | ||
| setPrivacyUrl(savedSettings.privacyUrl ?? '') | ||
| setHidePoweredBySim(savedSettings.hidePoweredBySim ?? false) | ||
| setLogoUrl(savedSettings.logoUrl ?? null) | ||
| setWordmarkUrl(savedSettings.wordmarkUrl ?? null) | ||
| setFormInitialized(true) | ||
| } |
There was a problem hiding this comment.
State updates during render body are a React anti-pattern
Calling multiple setState functions synchronously inside the component render body (rather than inside a useEffect) causes React to schedule an extra render immediately after the current one. In Strict Mode this compounds further. The formInitialized guard prevents infinite loops, but the idiomatic fix is a useEffect that depends on savedSettings:
useEffect(() => {
if (savedSettings && !formInitialized) {
setBrandName(savedSettings.brandName ?? '')
// ... rest of initialisation
setFormInitialized(true)
}
}, [savedSettings, formInitialized])…connected to starter (#4054)
