fix: code review fixes — date calc, input validation, rate limits, shared components

- Fix monthly_reset_at crash when billing anchor day exceeds next month's length
- Add environment_tags sanitization (max 20 tags, 100 chars each) to prevent prompt injection
- Add @limiter.limit("10/minute") rate limiting to all AI endpoints
- Use getTreeNavigatePath() routing helper instead of hardcoded paths
- Extract shared CreateFlowDropdown component from QuickStartPage and TreeLibraryPage
- Clear useCachedQuota on logout to prevent stale data across user sessions
- Add useRef guard to scaffold useEffect to prevent potential double-fire
- Use node.id as React key instead of array index in BranchDetailView
- Remove redundant dead logic in ai_tree_validator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-21 01:32:38 -05:00
parent 4b9863f22d
commit 37e1202f46
11 changed files with 163 additions and 182 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { Modal } from '@/components/common/Modal'
import { useAIFlowBuilderStore } from '@/store/aiFlowBuilderStore'
@@ -34,9 +34,11 @@ export function AIFlowBuilderModal({ isOpen, onClose }: AIFlowBuilderModalProps)
}
}, [isOpen, loadQuota])
// Auto-trigger scaffold after conversation starts
// Auto-trigger scaffold after conversation starts (ref prevents double-fire)
const hasTriggeredScaffold = useRef(false)
useEffect(() => {
if (phase === 'scaffolding' && !useAIFlowBuilderStore.getState().suggestedBranches.length) {
if (phase === 'scaffolding' && !hasTriggeredScaffold.current && !useAIFlowBuilderStore.getState().suggestedBranches.length) {
hasTriggeredScaffold.current = true
scaffold()
}
}, [phase, scaffold])

View File

@@ -200,8 +200,8 @@ function NodePreview({ node, depth }: { node: Record<string, unknown>; depth: nu
<span className="text-xs text-foreground truncate">{label}</span>
<span className="text-[10px] font-label text-muted-foreground">{type}</span>
</div>
{children.map((child, i) => (
<NodePreview key={i} node={child} depth={depth + 1} />
{children.map((child) => (
<NodePreview key={child.id as string ?? crypto.randomUUID()} node={child} depth={depth + 1} />
))}
</div>
)

View File

@@ -0,0 +1,93 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Plus, ChevronDown, Sparkles, FolderTree, ListOrdered, Wrench } from 'lucide-react'
import { cn } from '@/lib/utils'
interface CreateFlowDropdownProps {
aiEnabled: boolean
onOpenAIBuilder: () => void
className?: string
/** Button label — defaults to "Create Flow" */
label?: string
}
export function CreateFlowDropdown({
aiEnabled,
onOpenAIBuilder,
className,
label = 'Create Flow',
}: CreateFlowDropdownProps) {
const [showMenu, setShowMenu] = useState(false)
return (
<div className={cn('relative', className)}>
<button
onClick={() => setShowMenu(!showMenu)}
className="flex items-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity"
>
<Plus size={16} />
{label}
<ChevronDown size={14} />
</button>
{showMenu && (
<>
<div className="fixed inset-0 z-10" onClick={() => setShowMenu(false)} />
<div className="absolute right-0 z-20 mt-1 w-56 rounded-lg border border-border bg-card p-1 shadow-xl backdrop-blur-sm">
<Link
to="/trees/new"
onClick={() => setShowMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<FolderTree className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Troubleshooting Tree</div>
<div className="text-xs text-muted-foreground">Branching decision flow</div>
</div>
</Link>
<Link
to="/flows/new"
onClick={() => setShowMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<ListOrdered className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Procedural Flow</div>
<div className="text-xs text-muted-foreground">Step-by-step procedure</div>
</div>
</Link>
<Link
to="/flows/new?type=maintenance"
onClick={() => setShowMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Wrench className="h-4 w-4 text-amber-400" />
<div>
<div className="font-medium">Maintenance Flow</div>
<div className="text-xs text-muted-foreground">Scheduled multi-target tasks</div>
</div>
</Link>
{aiEnabled && (
<>
<div className="my-1 border-t border-border" />
<button
type="button"
onClick={() => {
setShowMenu(false)
onOpenAIBuilder()
}}
className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Sparkles className="h-4 w-4 text-primary" />
<div className="text-left">
<div className="font-medium">Build with AI</div>
<div className="text-xs text-muted-foreground">AI-assisted flow creation</div>
</div>
</button>
</>
)}
</div>
</>
)}
</div>
)
}