Skip to content

Commit 40eba79

Browse files
committed
Improve rate limit handling and query efficiency
Refactored Supabase rate limit logic for better exponential backoff and global state management. Updated cachedAuth to synchronize session cache and handle rate limit cooldowns more robustly. Optimized database queries in RebalanceHistoryTable and UnifiedAnalysisHistory to select only necessary fields, improving performance. Enhanced UI loading state in RebalanceHistoryTable and fixed day formatting in ScheduleListModal. Improved next run calculation in timeUtils to always return the earliest valid candidate.
1 parent 9a31357 commit 40eba79

File tree

7 files changed

+290
-512
lines changed

7 files changed

+290
-512
lines changed

src/components/Header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,15 @@ export default function Header() {
124124
<div className="container mx-auto px-6 py-4">
125125
<div className="flex items-center justify-between">
126126
<div className="flex items-center gap-2 sm:gap-4">
127-
<div className="flex items-center gap-2 sm:gap-3">
127+
<a className="flex items-center gap-2 sm:gap-3" href="/">
128128
<div className="p-2 rounded-lg">
129129
<img src="/goose.png" alt="TradingGoose Logo" className="h-8 w-8 sm:h-10 sm:w-10" />
130130
</div>
131131
<div>
132132
<h1 className="text-xl sm:text-2xl font-bold" style={{ color: '#FFCC00' }}>TradingGoose</h1>
133133
<p className="text-xs sm:text-sm text-muted-foreground">AI-Powered Portfolio Management</p>
134134
</div>
135-
</div>
135+
</a>
136136

137137
{isAuthenticated && (
138138
<div className="hidden md:flex items-center gap-1">

src/components/RebalanceHistoryTable.tsx

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ interface RebalanceAnalysis {
6363

6464
interface RebalanceRequest {
6565
id: string;
66-
user_id: string;
66+
user_id?: string;
6767
status: RebalanceStatus | string; // Support both new RebalanceStatus and legacy strings
6868
created_at: string;
6969
total_stocks: number;
@@ -185,7 +185,7 @@ export default function RebalanceHistoryTable() {
185185
try {
186186
const { data, error } = await supabase
187187
.from('analysis_history')
188-
.select('*')
188+
.select('id,ticker,analysis_status,full_analysis,created_at')
189189
.eq('rebalance_request_id', rebalanceId);
190190

191191
if (error) throw error;
@@ -214,7 +214,7 @@ export default function RebalanceHistoryTable() {
214214
// Query only rebalances from the selected date
215215
const { data, error } = await supabase
216216
.from('rebalance_requests')
217-
.select('*')
217+
.select('id,status,created_at,total_stocks,stocks_analyzed,plan_generated_at,error_message')
218218
.eq('user_id', user.id)
219219
.gte('created_at', startOfDay.toISOString())
220220
.lte('created_at', endOfDay.toISOString())
@@ -274,12 +274,19 @@ export default function RebalanceHistoryTable() {
274274
setCancelledRebalances(cancelled);
275275

276276
// Fetch analysis data for running rebalances to calculate progress
277-
const analysisDataMap: { [key: string]: any[] } = {};
278-
for (const rebalance of running) {
279-
const analyses = await fetchAnalysisDataForRebalance(rebalance.id);
280-
analysisDataMap[rebalance.id] = analyses;
277+
if (running.length > 0) {
278+
const analysisResults = await Promise.all(
279+
running.map((rebalance) => fetchAnalysisDataForRebalance(rebalance.id))
280+
);
281+
282+
const analysisDataMap: { [key: string]: any[] } = {};
283+
running.forEach((rebalance, index) => {
284+
analysisDataMap[rebalance.id] = analysisResults[index];
285+
});
286+
setAnalysisData(analysisDataMap);
287+
} else {
288+
setAnalysisData({});
281289
}
282-
setAnalysisData(analysisDataMap);
283290

284291
} catch (error) {
285292
console.error('Error fetching rebalance requests:', error);
@@ -688,13 +695,96 @@ export default function RebalanceHistoryTable() {
688695
if (loading) {
689696
return (
690697
<Card>
691-
<CardHeader>
692-
<CardTitle>Rebalance History</CardTitle>
693-
</CardHeader>
694-
<CardContent>
695-
<div className="flex items-center justify-center py-8">
696-
<Loader2 className="h-6 w-6 animate-spin" />
698+
<CardContent className="pt-6">
699+
<div className="flex items-center justify-between mb-4">
700+
<h3 className="text-lg font-semibold">Rebalance History</h3>
701+
<div className="flex items-center gap-2">
702+
{/* Manual refresh button */}
703+
<Button
704+
variant="ghost"
705+
size="sm"
706+
className="h-8 w-8 p-0 hover:bg-[#fc0]/10 hover:text-[#fc0]"
707+
title="Refresh"
708+
disabled
709+
>
710+
<RefreshCw className="h-4 w-4" />
711+
</Button>
712+
713+
<div className="w-px h-6 bg-border" />
714+
715+
<div className="flex items-center gap-1">
716+
<Button
717+
variant="ghost"
718+
size="sm"
719+
disabled
720+
className="h-8 w-8 p-0 hover:bg-[#fc0]/10 hover:text-[#fc0]"
721+
>
722+
<ChevronLeft className="h-4 w-4" />
723+
</Button>
724+
<Popover>
725+
<PopoverTrigger asChild>
726+
<Button
727+
variant="outline"
728+
size="sm"
729+
disabled
730+
className="justify-start text-left"
731+
>
732+
<CalendarIcon className="h-4 w-4 mr-2" />
733+
{getDateDisplay()}
734+
</Button>
735+
</PopoverTrigger>
736+
<PopoverContent className="w-auto p-0" align="start">
737+
<Calendar
738+
mode="single"
739+
selected={new Date(selectedDate)}
740+
initialFocus
741+
/>
742+
</PopoverContent>
743+
</Popover>
744+
<Button
745+
variant="ghost"
746+
size="sm"
747+
disabled
748+
className="h-8 w-8 p-0 hover:bg-[#fc0]/10 hover:text-[#fc0]"
749+
>
750+
<ChevronRight className="h-4 w-4" />
751+
</Button>
752+
</div>
753+
</div>
697754
</div>
755+
756+
<Tabs value={activeTab} className="space-y-4">
757+
<TabsList className="grid w-full grid-cols-4">
758+
<TabsTrigger value="all">All</TabsTrigger>
759+
<TabsTrigger value="running">Active</TabsTrigger>
760+
<TabsTrigger value="completed">Completed</TabsTrigger>
761+
<TabsTrigger value="canceled">Canceled</TabsTrigger>
762+
</TabsList>
763+
764+
<TabsContent value="all" className="space-y-4">
765+
<div className="flex items-center justify-center py-8">
766+
<Loader2 className="h-6 w-6 animate-spin" />
767+
</div>
768+
</TabsContent>
769+
770+
<TabsContent value="running">
771+
<div className="flex items-center justify-center py-8">
772+
<Loader2 className="h-6 w-6 animate-spin" />
773+
</div>
774+
</TabsContent>
775+
776+
<TabsContent value="completed">
777+
<div className="flex items-center justify-center py-8">
778+
<Loader2 className="h-6 w-6 animate-spin" />
779+
</div>
780+
</TabsContent>
781+
782+
<TabsContent value="canceled">
783+
<div className="flex items-center justify-center py-8">
784+
<Loader2 className="h-6 w-6 animate-spin" />
785+
</div>
786+
</TabsContent>
787+
</Tabs>
698788
</CardContent>
699789
</Card>
700790
);

src/components/ScheduleListModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ export default function ScheduleListModal({ isOpen, onClose }: ScheduleListModal
271271
const formatDaysList = (days?: number[]) => {
272272
if (!days || days.length === 0) return '';
273273
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
274-
return days.map(d => dayNames[d]).join(', ');
274+
return [...new Set(days)]
275+
.sort((a, b) => a - b)
276+
.map(d => dayNames[d])
277+
.join(', ');
275278
};
276279

277280
// State for storing calculated next run times and true UTC time
@@ -630,4 +633,4 @@ export default function ScheduleListModal({ isOpen, onClose }: ScheduleListModal
630633
</AlertDialog>
631634
</>
632635
);
633-
}
636+
}

src/components/UnifiedAnalysisHistory.tsx

Lines changed: 84 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ interface AnalysisHistoryItem {
6161
full_analysis?: any;
6262
created_at: string;
6363
analysis_status?: AnalysisStatus | number; // Support both new and legacy formats
64+
rebalance_request_id?: string | null;
65+
is_canceled?: boolean;
6466
}
6567

6668
interface RunningAnalysisItem {
@@ -235,10 +237,10 @@ export default function UnifiedAnalysisHistory() {
235237
const startOfDay = new Date(year, month - 1, day, 0, 0, 0, 0);
236238
const endOfDay = new Date(year, month - 1, day, 23, 59, 59, 999);
237239

238-
// Query only analyses from the selected date
240+
// Query only analyses from the selected date (summary fields only)
239241
const { data, error } = await supabase
240242
.from('analysis_history')
241-
.select('*')
243+
.select('id,ticker,analysis_date,decision,confidence,agent_insights,created_at,analysis_status,rebalance_request_id,is_canceled')
242244
.eq('user_id', user.id)
243245
.gte('created_at', startOfDay.toISOString())
244246
.lte('created_at', endOfDay.toISOString())
@@ -266,61 +268,92 @@ export default function UnifiedAnalysisHistory() {
266268
throw error;
267269
}
268270

269-
// Separate running, completed, and canceled analyses based on database data
270-
const runningAnalyses: RunningAnalysisItem[] = [];
271+
const baseRows = data ?? [];
272+
const runningIds: string[] = [];
273+
const runningBase: Array<Omit<RunningAnalysisItem, 'full_analysis'>> = [];
271274
const completedAnalyses: AnalysisHistoryItem[] = [];
272275
const canceledAnalyses: AnalysisHistoryItem[] = [];
273276

274-
for (const item of data || []) {
275-
// Use analysis_status field if available, otherwise fall back to old logic
276-
if ('analysis_status' in item) {
277-
// Convert legacy numeric status to new string format
278-
const status: AnalysisStatus = typeof item.analysis_status === 'number'
279-
? convertLegacyAnalysisStatus(item.analysis_status)
280-
: item.analysis_status as AnalysisStatus;
281-
282-
if (status === ANALYSIS_STATUS.RUNNING || status === ANALYSIS_STATUS.PENDING) {
283-
runningAnalyses.push({
284-
id: item.id,
285-
ticker: item.ticker,
286-
created_at: item.created_at,
287-
full_analysis: item.full_analysis,
288-
agent_insights: item.agent_insights, // Add this!
289-
rebalance_request_id: item.rebalance_request_id,
290-
status: status
291-
});
292-
} else if (status === ANALYSIS_STATUS.COMPLETED) {
293-
completedAnalyses.push(item);
294-
} else if (status === ANALYSIS_STATUS.ERROR || status === ANALYSIS_STATUS.CANCELLED) {
295-
// Show canceled/error analyses in the canceled section
296-
canceledAnalyses.push({
297-
...item,
298-
decision: status === ANALYSIS_STATUS.ERROR ? 'ERROR' : 'CANCELED',
299-
confidence: item.confidence || 0
300-
});
301-
}
302-
} else {
303-
// Fall back to old logic for backward compatibility
304-
const hasAgentInsights = item.agent_insights && Object.keys(item.agent_insights).length > 0;
305-
const isRunning = item.analysis_status === ANALYSIS_STATUS.RUNNING ||
306-
(item.confidence === 0 && !hasAgentInsights);
307-
308-
if (isRunning) {
309-
runningAnalyses.push({
310-
id: item.id,
311-
ticker: item.ticker,
312-
created_at: item.created_at,
313-
full_analysis: item.full_analysis,
314-
rebalance_request_id: item.rebalance_request_id,
315-
status: ANALYSIS_STATUS.RUNNING // Default to running for fallback logic
316-
});
317-
} else if ((item.confidence > 0 || hasAgentInsights) && item.decision && ['BUY', 'SELL', 'HOLD'].includes(item.decision)) {
318-
completedAnalyses.push(item);
277+
for (const item of baseRows) {
278+
let status: AnalysisStatus | null = null;
279+
280+
if (typeof item.analysis_status === 'number') {
281+
status = convertLegacyAnalysisStatus(item.analysis_status);
282+
} else if (typeof item.analysis_status === 'string') {
283+
status = item.analysis_status.toLowerCase() as AnalysisStatus;
284+
}
285+
286+
if (!status) {
287+
const hasAgentInsights = item.agent_insights && Object.keys(item.agent_insights || {}).length > 0;
288+
289+
if (item.is_canceled) {
290+
status = ANALYSIS_STATUS.CANCELLED;
291+
} else if (item.confidence === 0 && !hasAgentInsights) {
292+
status = ANALYSIS_STATUS.RUNNING;
293+
} else if (item.decision && ['BUY', 'SELL', 'HOLD'].includes(item.decision)) {
294+
status = ANALYSIS_STATUS.COMPLETED;
295+
} else {
296+
status = ANALYSIS_STATUS.RUNNING;
319297
}
320298
}
299+
300+
if (status === ANALYSIS_STATUS.RUNNING || status === ANALYSIS_STATUS.PENDING) {
301+
runningIds.push(item.id);
302+
runningBase.push({
303+
id: item.id,
304+
ticker: item.ticker,
305+
created_at: item.created_at,
306+
agent_insights: item.agent_insights,
307+
rebalance_request_id: item.rebalance_request_id || undefined,
308+
status
309+
});
310+
} else if (status === ANALYSIS_STATUS.COMPLETED) {
311+
completedAnalyses.push({
312+
id: item.id,
313+
ticker: item.ticker,
314+
analysis_date: item.analysis_date,
315+
decision: item.decision,
316+
confidence: item.confidence,
317+
agent_insights: item.agent_insights,
318+
created_at: item.created_at,
319+
analysis_status: status,
320+
rebalance_request_id: item.rebalance_request_id || undefined
321+
});
322+
} else if (status === ANALYSIS_STATUS.ERROR || status === ANALYSIS_STATUS.CANCELLED) {
323+
canceledAnalyses.push({
324+
id: item.id,
325+
ticker: item.ticker,
326+
analysis_date: item.analysis_date,
327+
decision: status === ANALYSIS_STATUS.ERROR ? 'ERROR' : 'CANCELED',
328+
confidence: item.confidence || 0,
329+
agent_insights: item.agent_insights,
330+
created_at: item.created_at,
331+
analysis_status: status,
332+
rebalance_request_id: item.rebalance_request_id || undefined
333+
});
334+
}
321335
}
322336

323-
setRunningAnalyses(runningAnalyses);
337+
let runningDetailsMap = new Map<string, any>();
338+
if (runningIds.length > 0) {
339+
const { data: runningDetails, error: runningDetailsError } = await supabase
340+
.from('analysis_history')
341+
.select('id, full_analysis')
342+
.in('id', runningIds);
343+
344+
if (!runningDetailsError && runningDetails) {
345+
runningDetailsMap = new Map(
346+
runningDetails.map(detail => [detail.id, detail.full_analysis])
347+
);
348+
}
349+
}
350+
351+
const hydratedRunningAnalyses: RunningAnalysisItem[] = runningBase.map(item => ({
352+
...item,
353+
full_analysis: runningDetailsMap.get(item.id) || null
354+
}));
355+
356+
setRunningAnalyses(hydratedRunningAnalyses);
324357
setHistory(completedAnalyses);
325358
setCanceledAnalyses(canceledAnalyses);
326359

@@ -390,7 +423,7 @@ export default function UnifiedAnalysisHistory() {
390423
// Only fetch the specific running analyses to check their status
391424
const { data, error } = await supabase
392425
.from('analysis_history')
393-
.select('*')
426+
.select('id,analysis_status,full_analysis,agent_insights,created_at')
394427
.eq('user_id', user.id)
395428
.in('id', runningIds)
396429
.gte('created_at', startOfDay.toISOString())

0 commit comments

Comments
 (0)