From ff9b5b2195c3ff11029009ffbb3c8dc161eaaa83 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 30 Mar 2026 04:56:10 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20remove=20hardcoded=20Sentry=20DSN=20?= =?UTF-8?q?fallback=20=E2=80=94=20use=20env=20var=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VITE_SENTRY_DSN is already set in Railway as a build arg. The hardcoded fallback was unnecessary and triggered GitHub secret scanning alerts. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/instrument.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/instrument.ts b/frontend/src/instrument.ts index 99c59e92..87ae697c 100644 --- a/frontend/src/instrument.ts +++ b/frontend/src/instrument.ts @@ -1,7 +1,7 @@ import * as Sentry from "@sentry/react"; Sentry.init({ - dsn: import.meta.env.VITE_SENTRY_DSN || "https://23937b8c0cea2484f6a9d5b97d0b7d4b@o4511005918887936.ingest.us.sentry.io/4511005926883328", + dsn: import.meta.env.VITE_SENTRY_DSN, environment: import.meta.env.MODE, integrations: [ From af524ec99dc9ad81f6445047f0e823cc49101917 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 30 Mar 2026 05:44:57 +0000 Subject: [PATCH 2/3] refactor: account settings page audit fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 15+ hard-coded color utilities with semantic tokens (text-danger, bg-success-dim, bg-warning-dim, bg-info-dim, etc.) - Fix progress bar track/fill color collision (bg-accent → bg-muted track) - Fix plan badge contrast: bg-accent text-muted-foreground → bg-accent-dim text-accent-text (WCAG AA violation) - Add aria-label to role-change select, remove member button, resend invite button - Add p-1 padding to icon-only buttons for minimum touch targets - Replace inline inviteError/inviteSuccess state with toast.success/error - Add toast feedback to handleSaveName and handleRemoveMember (were silent) - Fix transition-all → transition-colors on all 7 nav link cards - Fix hover:border-border (no-op) → hover:border-border-hover on nav cards - Consolidate 5 separate isAccountOwner nav card blocks under Team Settings section label for visual hierarchy - Remove duplicate embedded BrandingSettings component (Branding nav card exists) - Fix "tree categories" → "flow categories" (user-facing terminology) - Fix Target Lists description (remove reference to hidden maintenance flows) - Fix UsageStat label "Trees" → "Flows" - Remove orphaned BrandingSettings import and unused user store selector - Fix duplicate text-xs class in SSO Enterprise badge - Remove internal (Task 11) comment noise Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/AccountSettingsPage.tsx | 229 ++++++++++----------- 1 file changed, 105 insertions(+), 124 deletions(-) diff --git a/frontend/src/pages/AccountSettingsPage.tsx b/frontend/src/pages/AccountSettingsPage.tsx index 530236d7..3c47835d 100644 --- a/frontend/src/pages/AccountSettingsPage.tsx +++ b/frontend/src/pages/AccountSettingsPage.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug, Palette, ShieldCheck } from 'lucide-react' import { PageMeta } from '@/components/common/PageMeta' -import { BrandingSettings } from '@/components/settings/BrandingSettings' import { accountsApi } from '@/api/accounts' import type { Account, AccountMember, AccountInvite } from '@/types' import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal' @@ -22,7 +21,6 @@ export function AccountSettingsPage() { const { isAccountOwner } = usePermissions() const { plan, limits, usage } = useSubscription() const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore() - const user = useAuthStore((s) => s.user) const subscription = useAuthStore((s) => s.subscription) const [account, setAccount] = useState(null) @@ -45,8 +43,6 @@ export function AccountSettingsPage() { const [inviteEmail, setInviteEmail] = useState('') const [inviteRole, setInviteRole] = useState('engineer') const [isInviting, setIsInviting] = useState(false) - const [inviteError, setInviteError] = useState(null) - const [inviteSuccess, setInviteSuccess] = useState(null) useEffect(() => { loadData() @@ -86,7 +82,9 @@ export function AccountSettingsPage() { const updated = await accountsApi.updateMyAccount({ name: editedName.trim() }) setAccount(updated) setIsEditingName(false) + toast.success('Account name updated') } catch (err) { + toast.error('Failed to update account name') console.error('Failed to update account name:', err) } finally { setIsSavingName(false) @@ -98,17 +96,14 @@ export function AccountSettingsPage() { if (!inviteEmail.trim()) return setIsInviting(true) - setInviteError(null) - setInviteSuccess(null) try { await accountsApi.createInvite({ email: inviteEmail.trim(), role: inviteRole }) - setInviteSuccess(`Invitation sent to ${inviteEmail}`) + toast.success(`Invitation sent to ${inviteEmail}`) setInviteEmail('') - // Refresh invites list const invitesData = await accountsApi.getInvites() setInvites(invitesData) } catch (err) { - setInviteError('Failed to send invitation') + toast.error('Failed to send invitation') console.error(err) } finally { setIsInviting(false) @@ -135,7 +130,9 @@ export function AccountSettingsPage() { try { await accountsApi.removeMember(userId) setMembers(members.filter((m) => m.id !== userId)) + toast.success('Member removed') } catch (err) { + toast.error('Failed to remove member') console.error('Failed to remove member:', err) } } @@ -150,7 +147,7 @@ export function AccountSettingsPage() { if (error) { return ( -
+
{error} @@ -230,7 +227,7 @@ export function AccountSettingsPage() { {isAccountOwner && ( @@ -261,9 +258,9 @@ export function AccountSettingsPage() { @@ -273,11 +270,11 @@ export function AccountSettingsPage() { {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} @@ -295,7 +292,7 @@ export function AccountSettingsPage() { {limits && usage && (
@@ -350,12 +347,13 @@ export function AccountSettingsPage() {
{member.account_role === 'owner' ? ( - + owner ) : ( )} {!member.is_active && ( - + Inactive )} {member.account_role !== 'owner' && ( @@ -438,12 +436,6 @@ export function AccountSettingsPage() {
- {inviteError && ( -

{inviteError}

- )} - {inviteSuccess && ( -

{inviteSuccess}

- )} {/* Pending Invites */} @@ -467,14 +459,14 @@ export function AccountSettingsPage() {

- + {invite.role} @@ -774,7 +755,7 @@ function UsageStat({

{current} @@ -783,11 +764,11 @@ function UsageStat({

{!isUnlimited && ( -
+
From 5c11c1db337478268e1d16568746ffeacfcf252c Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 1 Apr 2026 00:41:50 +0000 Subject: [PATCH 3/3] fix: prevent stale selectChat async results from clobbering new session task lane Race condition: on page remount, selectChat(oldId) loads session data async. If the user clicks New Chat before the API returns, the old session's pending_task_lane was being applied to the new session's state, showing stale tasks and blocking new ones from appearing. Fix: currentChatRef tracks the most recently requested chat ID synchronously. All chat-creation paths (selectChat, handleNewChat, handleResumeNew) update it immediately. After each await in selectChat, bail if the ref no longer matches. Also documents the pattern as Lesson 106 in CLAUDE.md for future reference. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 +- frontend/src/pages/AssistantChatPage.tsx | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 78ab6de5..3ebb8970 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -373,7 +373,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi **105. `npm run build` fails with `EACCES: permission denied` on `dist/` in code-server:** This is a filesystem permission issue in the Docker environment, not a TypeScript error — the TS compilation completes successfully. Use `npx tsc -b` to verify TypeScript cleanly without needing to write to `dist/`. ---- +**106. Guard async "select item → load data → apply state" flows with a ref:** When a component lets the user switch between items (chat sessions, flows, scripts) and loads data asynchronously on each switch, the load for item A can complete *after* the user has already switched to item B — overwriting B's state with A's stale data. Fix pattern: keep a `currentSelectionRef = useRef(initialId)` and update it synchronously whenever the selection changes (in every creation/switch path). After every `await`, bail out if `currentSelectionRef.current !== thisItemId`. See `AssistantChatPage.tsx` `selectChat` for the reference implementation (`currentChatRef`). ## RBAC & Permissions diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index da71faa2..9b6169cb 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -81,6 +81,9 @@ export default function AssistantChatPage() { const fileInputRef = useRef(null) const dragCounterRef = useRef(0) const prefillHandledRef = useRef(false) + // Tracks the most recently requested active chat ID so in-flight selectChat + // calls that complete after the user switches chats don't clobber new state. + const currentChatRef = useRef(activeChatId) // Persist active chat ID to sessionStorage useEffect(() => { @@ -214,6 +217,7 @@ export default function AssistantChatPage() { } const selectChat = useCallback(async (chatId: string) => { + currentChatRef.current = chatId setActiveChatId(chatId) // Clear TaskLane when switching chats — will restore from backend if available setShowTaskLane(false) @@ -221,6 +225,10 @@ export default function AssistantChatPage() { setActiveActions([]) try { const detail = await aiSessionsApi.getSession(chatId) + // Guard: if the user switched to a different chat while this API call was + // in flight (e.g. clicked "New Chat"), discard stale results so we don't + // clobber the new session's task lane state. + if (currentChatRef.current !== chatId) return setMessages( (detail.conversation_messages || []).map(m => ({ role: m.role as 'user' | 'assistant', @@ -264,6 +272,7 @@ export default function AssistantChatPage() { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } + currentChatRef.current = session.session_id setChats(prev => [chatItem, ...prev]) setActiveChatId(session.session_id) setMessages([]) @@ -430,6 +439,7 @@ export default function AssistantChatPage() { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } + currentChatRef.current = session.session_id setChats(prev => [chatItem, ...prev]) setActiveChatId(session.session_id) setMessages([{ role: 'user', content: resumePrompt }])