refactor: move profile reads onto backend api
This commit is contained in:
parent
42420687f9
commit
535e0a88a9
@ -18,6 +18,7 @@ import {
|
|||||||
deleteTradeProfileForUser,
|
deleteTradeProfileForUser,
|
||||||
ensureDefaultTradeProfileForUser,
|
ensureDefaultTradeProfileForUser,
|
||||||
getCurrentUserProfile,
|
getCurrentUserProfile,
|
||||||
|
listAllTradeProfiles,
|
||||||
listTradeProfilesForUser,
|
listTradeProfilesForUser,
|
||||||
saveTradeProfileForUser,
|
saveTradeProfileForUser,
|
||||||
} from './profileRepository.js';
|
} from './profileRepository.js';
|
||||||
@ -1644,9 +1645,22 @@ export class ApiServer {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const ensureDefault = String(req.query.ensureDefault || '').toLowerCase() === 'true';
|
const ensureDefault = String(req.query.ensureDefault || '').toLowerCase() === 'true';
|
||||||
const profiles = ensureDefault
|
const scope = String(req.query.scope || 'user').toLowerCase();
|
||||||
? await ensureDefaultTradeProfileForUser(authUserId, supabaseService)
|
const wantsAll = scope === 'all';
|
||||||
: await listTradeProfilesForUser(authUserId, supabaseService);
|
const isAdmin = wantsAll ? await isTradingAdmin(authUserId, (req as AuthenticatedRequest).authRole) : false;
|
||||||
|
|
||||||
|
let profiles;
|
||||||
|
if (ensureDefault && !wantsAll) {
|
||||||
|
profiles = await ensureDefaultTradeProfileForUser(authUserId, supabaseService);
|
||||||
|
} else if (wantsAll) {
|
||||||
|
if (!isAdmin) {
|
||||||
|
res.status(403).json({ error: 'Forbidden: Admin role required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profiles = await listAllTradeProfiles(supabaseService);
|
||||||
|
} else {
|
||||||
|
profiles = await listTradeProfilesForUser(authUserId, supabaseService);
|
||||||
|
}
|
||||||
res.json({ profiles });
|
res.json({ profiles });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(500).json({ error: `Failed to load profiles: ${error.message}` });
|
res.status(500).json({ error: `Failed to load profiles: ${error.message}` });
|
||||||
|
|||||||
@ -123,6 +123,29 @@ async function listProfilesFromCosmos(userId: string): Promise<TradeProfileRecor
|
|||||||
.filter((profile): profile is TradeProfileRecord => Boolean(profile));
|
.filter((profile): profile is TradeProfileRecord => Boolean(profile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listAllProfilesFromCosmos(): Promise<TradeProfileRecord[]> {
|
||||||
|
if (!isCosmosConfigured()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = getContainer(PROFILE_CONTAINER);
|
||||||
|
const { resources } = await container.items.query<TradeProfileDocument>({
|
||||||
|
query: 'SELECT * FROM c WHERE c.productId = @productId AND c.type = @type ORDER BY c.createdAt DESC',
|
||||||
|
parameters: [
|
||||||
|
{ name: '@productId', value: config.PRODUCT_ID },
|
||||||
|
{ name: '@type', value: 'trade_profile' },
|
||||||
|
],
|
||||||
|
}).fetchAll();
|
||||||
|
|
||||||
|
return resources
|
||||||
|
.map((resource) => normalizeProfile({
|
||||||
|
...resource,
|
||||||
|
created_at: resource.createdAt,
|
||||||
|
updated_at: resource.updatedAt,
|
||||||
|
}))
|
||||||
|
.filter((profile): profile is TradeProfileRecord => Boolean(profile));
|
||||||
|
}
|
||||||
|
|
||||||
async function listProfilesFromSupabase(userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
async function listProfilesFromSupabase(userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
||||||
const client = legacyService?.getClient?.();
|
const client = legacyService?.getClient?.();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -149,6 +172,31 @@ async function listProfilesFromSupabase(userId: string, legacyService?: LegacySu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listAllProfilesFromSupabase(legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
||||||
|
const client = legacyService?.getClient?.();
|
||||||
|
if (!client) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('trade_profiles')
|
||||||
|
.select('id,user_id,name,allocated_capital,risk_per_trade_percent,symbols,is_active,strategy_config,created_at,updated_at')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error || !Array.isArray(data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
.map((row) => normalizeProfile(row as TradeProfileRecord))
|
||||||
|
.filter((profile): profile is TradeProfileRecord => Boolean(profile));
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[Profiles] Legacy global profile read failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function mirrorProfileToSupabase(profile: TradeProfileRecord, legacyService?: LegacySupabaseService): Promise<void> {
|
async function mirrorProfileToSupabase(profile: TradeProfileRecord, legacyService?: LegacySupabaseService): Promise<void> {
|
||||||
const client = legacyService?.getClient?.();
|
const client = legacyService?.getClient?.();
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
@ -202,6 +250,19 @@ export async function listTradeProfilesForUser(userId: string, legacyService?: L
|
|||||||
return listProfilesFromSupabase(userId, legacyService);
|
return listProfilesFromSupabase(userId, legacyService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listAllTradeProfiles(legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
||||||
|
try {
|
||||||
|
const cosmosProfiles = await listAllProfilesFromCosmos();
|
||||||
|
if (cosmosProfiles.length > 0) {
|
||||||
|
return cosmosProfiles;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[Profiles] Cosmos global profile read failed, falling back to legacy store: ${error instanceof Error ? error.message : 'unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return listAllProfilesFromSupabase(legacyService);
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureDefaultTradeProfileForUser(userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
export async function ensureDefaultTradeProfileForUser(userId: string, legacyService?: LegacySupabaseService): Promise<TradeProfileRecord[]> {
|
||||||
const profiles = await listTradeProfilesForUser(userId, legacyService);
|
const profiles = await listTradeProfilesForUser(userId, legacyService);
|
||||||
if (profiles.length > 0) {
|
if (profiles.length > 0) {
|
||||||
|
|||||||
@ -63,8 +63,15 @@ export async function fetchCurrentUserProfile(): Promise<CurrentUserProfile> {
|
|||||||
return response.profile;
|
return response.profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTradeProfiles(options?: { ensureDefault?: boolean }): Promise<TradeProfilePayload[]> {
|
export async function fetchTradeProfiles(options?: { ensureDefault?: boolean; scope?: 'user' | 'all' }): Promise<TradeProfilePayload[]> {
|
||||||
const query = options?.ensureDefault ? '?ensureDefault=true' : '';
|
const params = new URLSearchParams();
|
||||||
|
if (options?.ensureDefault) {
|
||||||
|
params.set('ensureDefault', 'true');
|
||||||
|
}
|
||||||
|
if (options?.scope === 'all') {
|
||||||
|
params.set('scope', 'all');
|
||||||
|
}
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
const response = await apiRequest<{ profiles: TradeProfilePayload[] }>(`/api/profiles${query}`);
|
const response = await apiRequest<{ profiles: TradeProfilePayload[] }>(`/api/profiles${query}`);
|
||||||
return Array.isArray(response.profiles) ? response.profiles : [];
|
return Array.isArray(response.profiles) ? response.profiles : [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import { tableNameProfiles } from '../lib/const';
|
|
||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
||||||
import { BacktestComparePanel } from '../backtest/components/BacktestComparePanel';
|
import { BacktestComparePanel } from '../backtest/components/BacktestComparePanel';
|
||||||
import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate';
|
import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate';
|
||||||
|
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||||
|
|
||||||
interface BacktestTabProps {
|
interface BacktestTabProps {
|
||||||
previewAsCustomer?: boolean;
|
previewAsCustomer?: boolean;
|
||||||
@ -31,20 +30,16 @@ export const BacktestTab: React.FC<BacktestTabProps> = ({ previewAsCustomer = fa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoadingProfiles(true);
|
setLoadingProfiles(true);
|
||||||
const { data, error } = await supabase
|
try {
|
||||||
.from(tableNameProfiles)
|
const data = await fetchTradeProfiles();
|
||||||
.select('*')
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
if (error) {
|
|
||||||
console.error('[BacktestTab] Failed to fetch profiles:', error);
|
|
||||||
setProfiles([]);
|
|
||||||
} else {
|
|
||||||
const nextProfiles = data || [];
|
const nextProfiles = data || [];
|
||||||
setProfiles(nextProfiles);
|
setProfiles(nextProfiles);
|
||||||
if (!nextProfiles.find((item: any) => item.id === selectedProfileId)) {
|
if (!nextProfiles.find((item: any) => item.id === selectedProfileId)) {
|
||||||
setSelectedProfileId(nextProfiles[0]?.id || '');
|
setSelectedProfileId(nextProfiles[0]?.id || '');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[BacktestTab] Failed to fetch profiles:', error);
|
||||||
|
setProfiles([]);
|
||||||
}
|
}
|
||||||
setLoadingProfiles(false);
|
setLoadingProfiles(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,14 +3,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { HistoryTab } from './HistoryTab';
|
import { HistoryTab } from './HistoryTab';
|
||||||
import { tableNameProfiles, tableNameTransactions } from '../lib/const';
|
import { tableNameTransactions } from '../lib/const';
|
||||||
|
|
||||||
const { authState } = vi.hoisted(() => ({
|
const { authState, fetchTradeProfilesMock } = vi.hoisted(() => ({
|
||||||
authState: {
|
authState: {
|
||||||
user: { id: 'u1' },
|
user: { id: 'u1' },
|
||||||
profile: { role: 'user' },
|
profile: { role: 'user' },
|
||||||
refreshProfile: vi.fn()
|
refreshProfile: vi.fn()
|
||||||
}
|
},
|
||||||
|
fetchTradeProfilesMock: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../components/AuthContext', () => ({
|
vi.mock('../components/AuthContext', () => ({
|
||||||
@ -34,6 +35,10 @@ vi.mock('../lib/supabaseClient', () => ({
|
|||||||
supabase: { from: vi.fn() }
|
supabase: { from: vi.fn() }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../lib/profileApi', () => ({
|
||||||
|
fetchTradeProfiles: fetchTradeProfilesMock
|
||||||
|
}));
|
||||||
|
|
||||||
import { supabase } from '../lib/supabaseClient';
|
import { supabase } from '../lib/supabaseClient';
|
||||||
|
|
||||||
describe('HistoryTab Master Suite', () => {
|
describe('HistoryTab Master Suite', () => {
|
||||||
@ -43,9 +48,9 @@ describe('HistoryTab Master Suite', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
fetchTradeProfilesMock.mockResolvedValue([{ id: 'p1', name: 'Alpha' }]);
|
||||||
(supabase.from as any).mockImplementation((table: string) => {
|
(supabase.from as any).mockImplementation((table: string) => {
|
||||||
if (table === tableNameTransactions) return mockSupabaseChain(historyData);
|
if (table === tableNameTransactions) return mockSupabaseChain(historyData);
|
||||||
if (table === tableNameProfiles) return mockSupabaseChain([{ id: 'p1', name: 'Alpha' }]);
|
|
||||||
return mockSupabaseChain([]);
|
return mockSupabaseChain([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -85,7 +90,6 @@ describe('HistoryTab Master Suite', () => {
|
|||||||
|
|
||||||
(supabase.from as any).mockImplementation((table: string) => {
|
(supabase.from as any).mockImplementation((table: string) => {
|
||||||
if (table === tableNameTransactions) return mockSupabaseChain(manyRecords);
|
if (table === tableNameTransactions) return mockSupabaseChain(manyRecords);
|
||||||
if (table === tableNameProfiles) return mockSupabaseChain([{ id: 'p1', name: 'Alpha' }]);
|
|
||||||
return mockSupabaseChain([]);
|
return mockSupabaseChain([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
import { supabase } from '../lib/supabaseClient';
|
||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import { tableNameTransactions, tableNameProfiles, tableNameOrders } from '../lib/const';
|
import { tableNameTransactions, tableNameOrders } from '../lib/const';
|
||||||
import { aggregateHistoryLedger, buildHistoryLedger } from '../lib/tradeHistoryLedger';
|
import { aggregateHistoryLedger, buildHistoryLedger } from '../lib/tradeHistoryLedger';
|
||||||
import {
|
import {
|
||||||
type LifecycleOrderRow
|
type LifecycleOrderRow
|
||||||
} from '../lib/orderLifecycleLedger';
|
} from '../lib/orderLifecycleLedger';
|
||||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||||
|
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||||
|
|
||||||
|
|
||||||
interface TradeRecord {
|
interface TradeRecord {
|
||||||
@ -353,18 +354,16 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
|
|||||||
historyData = histDataLegacy as TradeRecord[] | null;
|
historyData = histDataLegacy as TradeRecord[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let profilesQuery = supabase
|
let profData: Array<{ id: string; name: string; allocated_capital?: number }> = [];
|
||||||
.from(tableNameProfiles)
|
try {
|
||||||
.select('id, name, allocated_capital')
|
const profiles = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
|
||||||
.order('name', { ascending: true });
|
profData = profiles.map((item: any) => ({
|
||||||
|
id: String(item.id),
|
||||||
if (profile?.role !== 'admin') {
|
name: String(item.name || item.id || 'Unnamed Profile'),
|
||||||
profilesQuery = profilesQuery.eq('user_id', user.id);
|
allocated_capital: Number(item.allocated_capital || 0)
|
||||||
}
|
}));
|
||||||
|
} catch (error: any) {
|
||||||
const { data: profData, error: profError } = await profilesQuery;
|
console.error('[HistoryTab] Failed loading profiles:', error.message || error);
|
||||||
if (profError) {
|
|
||||||
console.error('[HistoryTab] Failed loading profiles:', profError.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderTagColumnsV2 = 'order_id,trade_id,profile_id,sub_tag,timestamp,filled_at,created_at,symbol,side,action,qty,quantity,price,status,source';
|
const orderTagColumnsV2 = 'order_id,trade_id,profile_id,sub_tag,timestamp,filled_at,created_at,symbol,side,action,qty,quantity,price,status,source';
|
||||||
|
|||||||
@ -3,27 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { OverviewTab, dedupeLivePositions } from './OverviewTab';
|
import { OverviewTab, dedupeLivePositions } from './OverviewTab';
|
||||||
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
||||||
import { tableNameProfiles } from '../lib/const';
|
|
||||||
|
|
||||||
const { authMock, supabaseMock, createMockQuery, canonicalLifecycleHookState } = vi.hoisted(() => {
|
|
||||||
const createMockQuery = (data: any, error: any = null) => {
|
|
||||||
const query: any = {
|
|
||||||
select: vi.fn(), eq: vi.fn(), order: vi.fn(), limit: vi.fn(), then: vi.fn()
|
|
||||||
};
|
|
||||||
query.select.mockReturnValue(query);
|
|
||||||
query.eq.mockReturnValue(query);
|
|
||||||
query.order.mockReturnValue(query);
|
|
||||||
query.limit.mockReturnValue(query);
|
|
||||||
query.then.mockImplementation((cb: any) => Promise.resolve({ data, error }).then(cb));
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const { authMock, fetchTradeProfilesMock, canonicalLifecycleHookState } = vi.hoisted(() => {
|
||||||
return {
|
return {
|
||||||
createMockQuery,
|
|
||||||
authMock: { user: { id: 'u1' }, profile: { role: 'user' } },
|
authMock: { user: { id: 'u1' }, profile: { role: 'user' } },
|
||||||
supabaseMock: {
|
fetchTradeProfilesMock: vi.fn(),
|
||||||
from: vi.fn()
|
|
||||||
},
|
|
||||||
canonicalLifecycleHookState: {
|
canonicalLifecycleHookState: {
|
||||||
snapshot: null as any,
|
snapshot: null as any,
|
||||||
loading: false,
|
loading: false,
|
||||||
@ -39,8 +23,8 @@ vi.mock('../components/AuthContext', () => ({
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/profileApi', () => ({
|
||||||
supabase: supabaseMock
|
fetchTradeProfiles: fetchTradeProfilesMock
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../hooks/useCanonicalLifecycle', () => ({
|
vi.mock('../hooks/useCanonicalLifecycle', () => ({
|
||||||
@ -70,13 +54,9 @@ describe('OverviewTab coverage maximization', () => {
|
|||||||
profileSignals: { 'p1': { signal: 'BUY', passed: true } }
|
profileSignals: { 'p1': { signal: 'BUY', passed: true } }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
fetchTradeProfilesMock.mockResolvedValue([
|
||||||
supabaseMock.from.mockImplementation((table: string) => {
|
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||||
if (table === tableNameProfiles) return createMockQuery([
|
]);
|
||||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
|
||||||
]);
|
|
||||||
return createMockQuery([]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('covers all uptime and duration formatting paths', async () => {
|
it('covers all uptime and duration formatting paths', async () => {
|
||||||
@ -99,7 +79,7 @@ describe('OverviewTab coverage maximization', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('covers fallback capital logic', async () => {
|
it('covers fallback capital logic', async () => {
|
||||||
supabaseMock.from.mockImplementation(() => createMockQuery([]));
|
fetchTradeProfilesMock.mockResolvedValue([]);
|
||||||
mockBotState.settings.totalCapital = 25000;
|
mockBotState.settings.totalCapital = 25000;
|
||||||
render(<OverviewTab botState={mockBotState} />);
|
render(<OverviewTab botState={mockBotState} />);
|
||||||
// The label in bot-status-bar is "Allocated:"
|
// The label in bot-status-bar is "Allocated:"
|
||||||
@ -107,22 +87,16 @@ describe('OverviewTab coverage maximization', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('covers Signal Active branches', async () => {
|
it('covers Signal Active branches', async () => {
|
||||||
supabaseMock.from.mockImplementation((table: string) => {
|
fetchTradeProfilesMock.mockResolvedValueOnce([
|
||||||
if (table === tableNameProfiles) return createMockQuery([
|
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 0, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 0, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
]);
|
||||||
]);
|
|
||||||
return createMockQuery([]);
|
|
||||||
});
|
|
||||||
const { unmount } = render(<OverviewTab botState={mockBotState} />);
|
const { unmount } = render(<OverviewTab botState={mockBotState} />);
|
||||||
await waitFor(() => expect(screen.getByText(/Signal active, no allocation/i)).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText(/Signal active, no allocation/i)).toBeInTheDocument());
|
||||||
unmount();
|
unmount();
|
||||||
|
|
||||||
supabaseMock.from.mockImplementation((table: string) => {
|
fetchTradeProfilesMock.mockResolvedValueOnce([
|
||||||
if (table === tableNameProfiles) return createMockQuery([
|
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
]);
|
||||||
]);
|
|
||||||
return createMockQuery([]);
|
|
||||||
});
|
|
||||||
render(<OverviewTab botState={mockBotState} />);
|
render(<OverviewTab botState={mockBotState} />);
|
||||||
await waitFor(() => expect(screen.getByText(/Signal active, waiting entry/i)).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText(/Signal active, waiting entry/i)).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
@ -225,12 +199,12 @@ describe('OverviewTab coverage maximization', () => {
|
|||||||
it('covers Admin role data fetching', async () => {
|
it('covers Admin role data fetching', async () => {
|
||||||
authMock.profile.role = 'admin';
|
authMock.profile.role = 'admin';
|
||||||
render(<OverviewTab botState={mockBotState} />);
|
render(<OverviewTab botState={mockBotState} />);
|
||||||
await waitFor(() => expect(supabaseMock.from).toHaveBeenCalled());
|
await waitFor(() => expect(fetchTradeProfilesMock).toHaveBeenCalledWith({ scope: 'all' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('covers error handling in fetchData', async () => {
|
it('covers error handling in fetchData', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
supabaseMock.from.mockImplementation(() => { throw new Error('Query Fail'); });
|
fetchTradeProfilesMock.mockRejectedValueOnce(new Error('Query Fail'));
|
||||||
render(<OverviewTab botState={mockBotState} />);
|
render(<OverviewTab botState={mockBotState} />);
|
||||||
await waitFor(() => expect(consoleSpy).toHaveBeenCalled());
|
await waitFor(() => expect(consoleSpy).toHaveBeenCalled());
|
||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import type { BotState } from '../hooks/useWebSocket';
|
import type { BotState } from '../hooks/useWebSocket';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import { tableNameProfiles } from '../lib/const';
|
|
||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import { aggregateCanonicalLifecycleTrades } from '../lib/orderLifecycleLedger';
|
import { aggregateCanonicalLifecycleTrades } from '../lib/orderLifecycleLedger';
|
||||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||||
|
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||||
|
|
||||||
interface OverviewTabProps {
|
interface OverviewTabProps {
|
||||||
botState: BotState;
|
botState: BotState;
|
||||||
@ -133,19 +132,7 @@ export const OverviewTab = ({ botState, previewAsCustomer = false, connected = t
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let profilesQuery = supabase
|
const profilesData = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
|
||||||
.from(tableNameProfiles)
|
|
||||||
.select('id, name, allocated_capital, is_active, strategy_config');
|
|
||||||
|
|
||||||
if (profile?.role !== 'admin') {
|
|
||||||
profilesQuery = profilesQuery.eq('user_id', user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: profilesData, error: profilesError } = await profilesQuery;
|
|
||||||
|
|
||||||
if (profilesError) {
|
|
||||||
console.error('[OverviewTab] Failed loading profiles:', profilesError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeProfileRows: ActiveProfileCapital[] = ((profilesData as any[]) || [])
|
const activeProfileRows: ActiveProfileCapital[] = ((profilesData as any[]) || [])
|
||||||
.filter((p: any) => Boolean(p.is_active))
|
.filter((p: any) => Boolean(p.is_active))
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { PositionsTab } from './PositionsTab';
|
import { PositionsTab } from './PositionsTab';
|
||||||
import type { BotState } from '../hooks/useWebSocket';
|
import type { BotState } from '../hooks/useWebSocket';
|
||||||
import { tableNameOrders, tableNameProfiles, tableNameStocks } from '../lib/const';
|
import { tableNameOrders, tableNameStocks } from '../lib/const';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
authState,
|
authState,
|
||||||
@ -13,7 +13,7 @@ const {
|
|||||||
entriesEqMock,
|
entriesEqMock,
|
||||||
ordersEqMock,
|
ordersEqMock,
|
||||||
historyEqMock,
|
historyEqMock,
|
||||||
profilesEqMock
|
fetchTradeProfilesMock
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
authState: {
|
authState: {
|
||||||
user: { id: 'user-1' } as any,
|
user: { id: 'user-1' } as any,
|
||||||
@ -24,7 +24,7 @@ const {
|
|||||||
entriesEqMock: vi.fn(),
|
entriesEqMock: vi.fn(),
|
||||||
ordersEqMock: vi.fn(),
|
ordersEqMock: vi.fn(),
|
||||||
historyEqMock: vi.fn(),
|
historyEqMock: vi.fn(),
|
||||||
profilesEqMock: vi.fn()
|
fetchTradeProfilesMock: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../components/AuthContext', () => ({
|
vi.mock('../components/AuthContext', () => ({
|
||||||
@ -40,12 +40,16 @@ vi.mock('../lib/supabaseClient', () => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../lib/profileApi', () => ({
|
||||||
|
fetchTradeProfiles: fetchTradeProfilesMock
|
||||||
|
}));
|
||||||
|
|
||||||
interface QueryResult {
|
interface QueryResult {
|
||||||
data: any;
|
data: any;
|
||||||
error: any;
|
error: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TableKey = 'entries' | 'orders' | 'trade_history' | 'trade_profiles';
|
type TableKey = 'entries' | 'orders' | 'trade_history';
|
||||||
|
|
||||||
const makeBuilder = (result: QueryResult, eqSpy: (field: string, value: any) => void) => {
|
const makeBuilder = (result: QueryResult, eqSpy: (field: string, value: any) => void) => {
|
||||||
const builder: any = {
|
const builder: any = {
|
||||||
@ -65,8 +69,7 @@ const configureQueries = (plan: Record<TableKey, QueryResult[]>) => {
|
|||||||
const queues = {
|
const queues = {
|
||||||
entries: [...plan.entries],
|
entries: [...plan.entries],
|
||||||
orders: [...plan.orders],
|
orders: [...plan.orders],
|
||||||
trade_history: [...plan.trade_history],
|
trade_history: [...plan.trade_history]
|
||||||
trade_profiles: [...plan.trade_profiles]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fromMock.mockImplementation((table: string) => {
|
fromMock.mockImplementation((table: string) => {
|
||||||
@ -79,9 +82,6 @@ const configureQueries = (plan: Record<TableKey, QueryResult[]>) => {
|
|||||||
if (table === 'trade_history') {
|
if (table === 'trade_history') {
|
||||||
return makeBuilder(queues.trade_history.shift() || { data: [], error: null }, historyEqMock);
|
return makeBuilder(queues.trade_history.shift() || { data: [], error: null }, historyEqMock);
|
||||||
}
|
}
|
||||||
if (table === tableNameProfiles) {
|
|
||||||
return makeBuilder(queues.trade_profiles.shift() || { data: [], error: null }, profilesEqMock);
|
|
||||||
}
|
|
||||||
return makeBuilder({ data: [], error: null }, vi.fn());
|
return makeBuilder({ data: [], error: null }, vi.fn());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -262,7 +262,11 @@ describe('PositionsTab DOM behavior', () => {
|
|||||||
entriesEqMock.mockReset();
|
entriesEqMock.mockReset();
|
||||||
ordersEqMock.mockReset();
|
ordersEqMock.mockReset();
|
||||||
historyEqMock.mockReset();
|
historyEqMock.mockReset();
|
||||||
profilesEqMock.mockReset();
|
fetchTradeProfilesMock.mockReset();
|
||||||
|
fetchTradeProfilesMock.mockResolvedValue([
|
||||||
|
{ id: 'p1', name: 'High Risk Scalper' },
|
||||||
|
{ id: 'p2', name: 'Conservative Bag' }
|
||||||
|
]);
|
||||||
|
|
||||||
vi.stubGlobal('confirm', vi.fn(() => true));
|
vi.stubGlobal('confirm', vi.fn(() => true));
|
||||||
vi.stubGlobal('alert', vi.fn());
|
vi.stubGlobal('alert', vi.fn());
|
||||||
@ -361,13 +365,6 @@ describe('PositionsTab DOM behavior', () => {
|
|||||||
trade_history: [{
|
trade_history: [{
|
||||||
data: [{ trade_id: 'TRD-BOT-3', profile_id: 'p2' }, { trade_id: '', profile_id: 'p1' }],
|
data: [{ trade_id: 'TRD-BOT-3', profile_id: 'p2' }, { trade_id: '', profile_id: 'p1' }],
|
||||||
error: null
|
error: null
|
||||||
}],
|
|
||||||
trade_profiles: [{
|
|
||||||
data: [
|
|
||||||
{ id: 'p1', name: 'High Risk Scalper' },
|
|
||||||
{ id: 'p2', name: 'Conservative Bag' }
|
|
||||||
],
|
|
||||||
error: null
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -449,7 +446,6 @@ describe('PositionsTab DOM behavior', () => {
|
|||||||
error: null
|
error: null
|
||||||
}],
|
}],
|
||||||
trade_history: [{ data: [], error: null }],
|
trade_history: [{ data: [], error: null }],
|
||||||
trade_profiles: [{ data: [{ id: 'p1', name: 'High Risk Scalper' }], error: null }]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
@ -507,9 +503,9 @@ describe('PositionsTab DOM behavior', () => {
|
|||||||
{ data: null, error: { message: 'v2 fallback failed' } },
|
{ data: null, error: { message: 'v2 fallback failed' } },
|
||||||
{ data: null, error: { message: 'legacy failed' } }
|
{ data: null, error: { message: 'legacy failed' } }
|
||||||
],
|
],
|
||||||
trade_history: [{ data: null, error: { message: 'history failed' } }],
|
trade_history: [{ data: null, error: { message: 'history failed' } }]
|
||||||
trade_profiles: [{ data: null, error: { message: 'profiles failed' } }]
|
|
||||||
});
|
});
|
||||||
|
fetchTradeProfilesMock.mockRejectedValueOnce(new Error('profiles failed'));
|
||||||
|
|
||||||
render(<PositionsTab botState={{
|
render(<PositionsTab botState={{
|
||||||
symbols: {},
|
symbols: {},
|
||||||
@ -536,7 +532,7 @@ describe('PositionsTab DOM behavior', () => {
|
|||||||
expect(entriesEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
expect(entriesEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
||||||
expect(ordersEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
expect(ordersEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
||||||
expect(historyEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
expect(historyEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
||||||
expect(profilesEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
expect(fetchTradeProfilesMock).toHaveBeenCalledWith({ scope: 'user' });
|
||||||
|
|
||||||
expect(warnSpy).toHaveBeenCalledWith(
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
'[PositionsTab] V3 order query failed, falling back to v2 columns:',
|
'[PositionsTab] V3 order query failed, falling back to v2 columns:',
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import type { BotState } from '../hooks/useWebSocket';
|
|||||||
import { supabase } from '../lib/supabaseClient';
|
import { supabase } from '../lib/supabaseClient';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import { tableNameStocks, tableNameOrders, tableNameProfiles } from '../lib/const';
|
import { tableNameStocks, tableNameOrders } from '../lib/const';
|
||||||
import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
|
import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
|
||||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||||
|
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||||
|
|
||||||
interface PositionsTabProps {
|
interface PositionsTabProps {
|
||||||
botState: BotState;
|
botState: BotState;
|
||||||
@ -523,17 +524,18 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
|
|||||||
console.warn('[PositionsTab] History trade lookup failed:', histError.message);
|
console.warn('[PositionsTab] History trade lookup failed:', histError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let profilesQuery = supabase
|
let profData: Array<{ id: string; name: string }> = [];
|
||||||
.from(tableNameProfiles)
|
let profError: Error | null = null;
|
||||||
.select('id, name')
|
try {
|
||||||
.order('name', { ascending: true });
|
const profiles = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
|
||||||
|
profData = profiles.map((item: any) => ({
|
||||||
if (profile?.role !== 'admin') {
|
id: String(item.id),
|
||||||
profilesQuery = profilesQuery.eq('user_id', user.id);
|
name: String(item.name || item.id || 'Unnamed Profile')
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
profError = error as Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: profData, error: profError } = await profilesQuery;
|
|
||||||
|
|
||||||
if (posError) {
|
if (posError) {
|
||||||
console.error('[PositionsTab] Failed loading manual positions:', posError.message);
|
console.error('[PositionsTab] Failed loading manual positions:', posError.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user