Skip to content

Commit e837f62

Browse files
committed
Add near-limit analysis RBAC and improve history handling
Introduces a new RBAC flag for near-limit analysis access, updating role management, admin UI, and settings to support it. Enhances history tables and workflows with improved refresh, cancellation, and deletion logic for rebalances and analyses. Also improves 1D stock chart data handling and adds a date picker to the trade history table.
1 parent f7e2c8e commit e837f62

File tree

16 files changed

+420
-76
lines changed

16 files changed

+420
-76
lines changed

src/components/RebalanceHistoryTable.tsx

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default function RebalanceHistoryTable() {
8080
const { user } = useAuth();
8181
const { toast } = useToast();
8282
const [loading, setLoading] = useState(true);
83+
const [refreshing, setRefreshing] = useState(false); // Separate state for refresh button
8384
const [runningRebalances, setRunningRebalances] = useState<RebalanceRequest[]>([]);
8485
const [completedRebalances, setCompletedRebalances] = useState<RebalanceRequest[]>([]);
8586
const [cancelledRebalances, setCancelledRebalances] = useState<RebalanceRequest[]>([]);
@@ -125,7 +126,8 @@ export default function RebalanceHistoryTable() {
125126

126127
useEffect(() => {
127128
if (user) {
128-
fetchRebalanceRequests();
129+
// Pass true for isInitialLoad only when we haven't loaded data yet
130+
fetchRebalanceRequests(!initialLoadComplete);
129131
}
130132
}, [user, selectedDate]); // Reload when selectedDate changes
131133

@@ -149,15 +151,15 @@ export default function RebalanceHistoryTable() {
149151
(payload) => {
150152
// Only fetch if it's an update to a running rebalance
151153
if (payload.new && runningRebalances.some(r => r.id === payload.new.id)) {
152-
fetchRebalanceRequests();
154+
fetchRebalanceRequests(false); // Don't show loading on subscription updates
153155
}
154156
}
155157
)
156158
.subscribe();
157159

158160
// Much slower polling - every 15 seconds instead of 3 seconds
159161
const interval = setInterval(() => {
160-
fetchRebalanceRequests();
162+
fetchRebalanceRequests(false); // Don't show loading on polling updates
161163
}, 15000); // Poll every 15 seconds for running rebalances
162164

163165
return () => {
@@ -166,6 +168,12 @@ export default function RebalanceHistoryTable() {
166168
};
167169
}, [user, runningRebalances.length > 0, selectedDate, initialLoadComplete]); // Use boolean comparison
168170

171+
const handleManualRefresh = async () => {
172+
setRefreshing(true);
173+
await fetchRebalanceRequests(false);
174+
setRefreshing(false);
175+
};
176+
169177
const fetchAnalysisDataForRebalance = async (rebalanceId: string) => {
170178
try {
171179
const { data, error } = await supabase
@@ -182,9 +190,14 @@ export default function RebalanceHistoryTable() {
182190
}
183191
};
184192

185-
const fetchRebalanceRequests = async () => {
193+
const fetchRebalanceRequests = async (isInitialLoad = false) => {
186194
if (!user) return;
187195

196+
// Only show loading state on initial load or when explicitly requested
197+
if (isInitialLoad) {
198+
setLoading(true);
199+
}
200+
188201
try {
189202
// Build date range for the selected date using local date parsing
190203
const [year, month, day] = selectedDate.split('-').map(Number);
@@ -200,7 +213,27 @@ export default function RebalanceHistoryTable() {
200213
.lte('created_at', endOfDay.toISOString())
201214
.order('created_at', { ascending: false });
202215

203-
if (error) throw error;
216+
if (error) {
217+
console.error('Error fetching rebalance requests:', error);
218+
219+
// Handle 500 errors gracefully - don't throw, just return
220+
if (error.message?.includes('500') || error.code === '500') {
221+
console.log('Server error, skipping update');
222+
return;
223+
}
224+
225+
// Handle authentication errors
226+
if (error.code === 'PGRST301' || error.message?.includes('JWT')) {
227+
console.log('JWT error detected, will attempt refresh on next poll');
228+
// Try to refresh the session
229+
supabase.auth.refreshSession().catch(err => {
230+
console.error('Failed to refresh session:', err);
231+
});
232+
return;
233+
}
234+
235+
throw error;
236+
}
204237

205238
// Debug logging
206239
console.log('Fetched rebalance requests:', data?.map(r => ({
@@ -243,32 +276,43 @@ export default function RebalanceHistoryTable() {
243276

244277
} catch (error) {
245278
console.error('Error fetching rebalance requests:', error);
246-
if (!loading) {
279+
if (!initialLoadComplete) { // Only show toast if not initial load
247280
toast({
248281
title: 'Error Loading History',
249282
description: 'Failed to load rebalance history. Please try again.',
250283
variant: 'destructive'
251284
});
252285
}
253286
} finally {
254-
setLoading(false);
287+
if (isInitialLoad) {
288+
setLoading(false);
289+
}
255290
setInitialLoadComplete(true);
256291
}
257292
};
258293

259294
const handleDelete = async () => {
260-
if (!selectedRebalanceId) return;
295+
if (!selectedRebalanceId || !user) return;
261296

262297
setDeleting(true);
263298
try {
299+
const { error: tradingError } = await supabase
300+
.from('trading_actions')
301+
.delete()
302+
.eq('rebalance_request_id', selectedRebalanceId)
303+
.eq('user_id', user.id);
304+
305+
if (tradingError && tradingError.code !== '23503') {
306+
throw tradingError;
307+
}
308+
264309
// Delete related analyses first from analysis_history table
265310
const { error: analysesError } = await supabase
266311
.from('analysis_history')
267312
.delete()
268313
.eq('rebalance_request_id', selectedRebalanceId);
269314

270315
if (analysesError && analysesError.code !== '23503') {
271-
// Ignore foreign key constraint errors as cascade delete should handle it
272316
console.warn('Error deleting related analyses:', analysesError);
273317
}
274318

@@ -308,10 +352,16 @@ export default function RebalanceHistoryTable() {
308352

309353
setCancelling(true);
310354
try {
355+
// Find the item to be cancelled
356+
const itemToCancel = runningRebalances.find(r => r.id === selectedRebalanceId);
357+
if (!itemToCancel) {
358+
throw new Error('Rebalance not found in running list');
359+
}
360+
311361
// First check if the rebalance exists and belongs to the user
312362
const { data: checkData, error: checkError } = await supabase
313363
.from('rebalance_requests')
314-
.select('id, status')
364+
.select('*')
315365
.eq('id', selectedRebalanceId)
316366
.eq('user_id', user.id)
317367
.single();
@@ -341,25 +391,59 @@ export default function RebalanceHistoryTable() {
341391
const { error: updateError } = await supabase
342392
.from('rebalance_requests')
343393
.update({
344-
status: REBALANCE_STATUS.CANCELLED
394+
status: REBALANCE_STATUS.CANCELLED,
395+
error_message: 'Cancelled by user'
345396
})
346397
.eq('id', selectedRebalanceId)
347398
.eq('user_id', user.id);
348399

349400
if (updateError) throw updateError;
350401
}
351402

403+
// Gracefully move from running to cancelled list
404+
setRunningRebalances(prev => prev.filter(r => r.id !== selectedRebalanceId));
405+
406+
// Create the cancelled item with all data from the database
407+
const cancelledItem: RebalanceRequest = {
408+
...checkData,
409+
status: REBALANCE_STATUS.CANCELLED,
410+
error_message: checkData.error_message || 'Cancelled by user'
411+
};
412+
413+
setCancelledRebalances(prev => [cancelledItem, ...prev]);
414+
415+
// Also cancel all related analyses if any
416+
if (analysisData[selectedRebalanceId]) {
417+
const analysesToCancel = analysisData[selectedRebalanceId];
418+
// Update analysis statuses in the background
419+
analysesToCancel.forEach(async (analysis: any) => {
420+
await supabase
421+
.from('analysis_history')
422+
.update({
423+
analysis_status: 'CANCELLED',
424+
updated_at: new Date().toISOString()
425+
})
426+
.eq('id', analysis.id)
427+
.eq('rebalance_request_id', selectedRebalanceId);
428+
});
429+
430+
// Remove from analysis data
431+
setAnalysisData(prev => {
432+
const newData = { ...prev };
433+
delete newData[selectedRebalanceId];
434+
return newData;
435+
});
436+
}
352437

353438
toast({
354439
title: 'Success',
355440
description: 'Rebalance cancelled successfully'
356441
});
357442

358-
// Move from running to cancelled instead of refreshing everything
359-
const cancelledItem = runningRebalances.find(r => r.id === selectedRebalanceId);
360-
if (cancelledItem) {
361-
setRunningRebalances(prev => prev.filter(r => r.id !== selectedRebalanceId));
362-
setCancelledRebalances(prev => [{...cancelledItem, status: REBALANCE_STATUS.CANCELLED}, ...prev]);
443+
// Close modal if viewing this rebalance
444+
if (selectedDetailId === selectedRebalanceId) {
445+
setDetailModalOpen(false);
446+
setSelectedDetailId(undefined);
363447
}
364448
} catch (error: any) {
365449
console.error('Error cancelling rebalance:', error);
@@ -595,12 +679,12 @@ export default function RebalanceHistoryTable() {
595679
<Button
596680
variant="ghost"
597681
size="sm"
598-
onClick={() => fetchRebalanceRequests()}
599-
disabled={loading}
682+
onClick={handleManualRefresh}
683+
disabled={refreshing}
600684
className="h-8 w-8 p-0 hover:bg-[#fc0]/10 hover:text-[#fc0]"
601685
title="Refresh"
602686
>
603-
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
687+
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
604688
</Button>
605689

606690
<div className="w-px h-6 bg-border" />
@@ -1406,4 +1490,4 @@ export default function RebalanceHistoryTable() {
14061490
/>
14071491
</>
14081492
);
1409-
}
1493+
}

0 commit comments

Comments
 (0)