refactor: move profile reads onto backend api
This commit is contained in:
parent
42420687f9
commit
535e0a88a9
@ -18,6 +18,7 @@ import {
|
||||
deleteTradeProfileForUser,
|
||||
ensureDefaultTradeProfileForUser,
|
||||
getCurrentUserProfile,
|
||||
listAllTradeProfiles,
|
||||
listTradeProfilesForUser,
|
||||
saveTradeProfileForUser,
|
||||
} from './profileRepository.js';
|
||||
@ -1644,9 +1645,22 @@ export class ApiServer {
|
||||
|
||||
try {
|
||||
const ensureDefault = String(req.query.ensureDefault || '').toLowerCase() === 'true';
|
||||
const profiles = ensureDefault
|
||||
? await ensureDefaultTradeProfileForUser(authUserId, supabaseService)
|
||||
: await listTradeProfilesForUser(authUserId, supabaseService);
|
||||
const scope = String(req.query.scope || 'user').toLowerCase();
|
||||
const wantsAll = scope === 'all';
|
||||
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 });
|
||||
} catch (error: any) {
|
||||
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));
|
||||
}
|
||||
|
||||
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[]> {
|
||||
const client = legacyService?.getClient?.();
|
||||
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> {
|
||||
const client = legacyService?.getClient?.();
|
||||
if (!client) return;
|
||||
@ -202,6 +250,19 @@ export async function listTradeProfilesForUser(userId: string, legacyService?: L
|
||||
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[]> {
|
||||
const profiles = await listTradeProfilesForUser(userId, legacyService);
|
||||
if (profiles.length > 0) {
|
||||
|
||||
@ -63,8 +63,15 @@ export async function fetchCurrentUserProfile(): Promise<CurrentUserProfile> {
|
||||
return response.profile;
|
||||
}
|
||||
|
||||
export async function fetchTradeProfiles(options?: { ensureDefault?: boolean }): Promise<TradeProfilePayload[]> {
|
||||
const query = options?.ensureDefault ? '?ensureDefault=true' : '';
|
||||
export async function fetchTradeProfiles(options?: { ensureDefault?: boolean; scope?: 'user' | 'all' }): Promise<TradeProfilePayload[]> {
|
||||
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}`);
|
||||
return Array.isArray(response.profiles) ? response.profiles : [];
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { supabase } from '../lib/supabaseClient';
|
||||
import { tableNameProfiles } from '../lib/const';
|
||||
import { useAuth } from '../components/AuthContext';
|
||||
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
||||
import { BacktestComparePanel } from '../backtest/components/BacktestComparePanel';
|
||||
import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate';
|
||||
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||
|
||||
interface BacktestTabProps {
|
||||
previewAsCustomer?: boolean;
|
||||
@ -31,20 +30,16 @@ export const BacktestTab: React.FC<BacktestTabProps> = ({ previewAsCustomer = fa
|
||||
return;
|
||||
}
|
||||
setLoadingProfiles(true);
|
||||
const { data, error } = await supabase
|
||||
.from(tableNameProfiles)
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false });
|
||||
if (error) {
|
||||
console.error('[BacktestTab] Failed to fetch profiles:', error);
|
||||
setProfiles([]);
|
||||
} else {
|
||||
try {
|
||||
const data = await fetchTradeProfiles();
|
||||
const nextProfiles = data || [];
|
||||
setProfiles(nextProfiles);
|
||||
if (!nextProfiles.find((item: any) => item.id === selectedProfileId)) {
|
||||
setSelectedProfileId(nextProfiles[0]?.id || '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BacktestTab] Failed to fetch profiles:', error);
|
||||
setProfiles([]);
|
||||
}
|
||||
setLoadingProfiles(false);
|
||||
};
|
||||
|
||||
@ -3,14 +3,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
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: {
|
||||
user: { id: 'u1' },
|
||||
profile: { role: 'user' },
|
||||
refreshProfile: vi.fn()
|
||||
}
|
||||
},
|
||||
fetchTradeProfilesMock: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('../components/AuthContext', () => ({
|
||||
@ -34,6 +35,10 @@ vi.mock('../lib/supabaseClient', () => ({
|
||||
supabase: { from: vi.fn() }
|
||||
}));
|
||||
|
||||
vi.mock('../lib/profileApi', () => ({
|
||||
fetchTradeProfiles: fetchTradeProfilesMock
|
||||
}));
|
||||
|
||||
import { supabase } from '../lib/supabaseClient';
|
||||
|
||||
describe('HistoryTab Master Suite', () => {
|
||||
@ -43,9 +48,9 @@ describe('HistoryTab Master Suite', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
fetchTradeProfilesMock.mockResolvedValue([{ id: 'p1', name: 'Alpha' }]);
|
||||
(supabase.from as any).mockImplementation((table: string) => {
|
||||
if (table === tableNameTransactions) return mockSupabaseChain(historyData);
|
||||
if (table === tableNameProfiles) return mockSupabaseChain([{ id: 'p1', name: 'Alpha' }]);
|
||||
return mockSupabaseChain([]);
|
||||
});
|
||||
});
|
||||
@ -85,7 +90,6 @@ describe('HistoryTab Master Suite', () => {
|
||||
|
||||
(supabase.from as any).mockImplementation((table: string) => {
|
||||
if (table === tableNameTransactions) return mockSupabaseChain(manyRecords);
|
||||
if (table === tableNameProfiles) return mockSupabaseChain([{ id: 'p1', name: 'Alpha' }]);
|
||||
return mockSupabaseChain([]);
|
||||
});
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { supabase } from '../lib/supabaseClient';
|
||||
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 {
|
||||
type LifecycleOrderRow
|
||||
} from '../lib/orderLifecycleLedger';
|
||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||
|
||||
|
||||
interface TradeRecord {
|
||||
@ -353,18 +354,16 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
|
||||
historyData = histDataLegacy as TradeRecord[] | null;
|
||||
}
|
||||
|
||||
let profilesQuery = supabase
|
||||
.from(tableNameProfiles)
|
||||
.select('id, name, allocated_capital')
|
||||
.order('name', { ascending: true });
|
||||
|
||||
if (profile?.role !== 'admin') {
|
||||
profilesQuery = profilesQuery.eq('user_id', user.id);
|
||||
}
|
||||
|
||||
const { data: profData, error: profError } = await profilesQuery;
|
||||
if (profError) {
|
||||
console.error('[HistoryTab] Failed loading profiles:', profError.message);
|
||||
let profData: Array<{ id: string; name: string; allocated_capital?: number }> = [];
|
||||
try {
|
||||
const profiles = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
|
||||
profData = profiles.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: String(item.name || item.id || 'Unnamed Profile'),
|
||||
allocated_capital: Number(item.allocated_capital || 0)
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error('[HistoryTab] Failed loading profiles:', error.message || error);
|
||||
}
|
||||
|
||||
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 { OverviewTab, dedupeLivePositions } from './OverviewTab';
|
||||
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 {
|
||||
createMockQuery,
|
||||
authMock: { user: { id: 'u1' }, profile: { role: 'user' } },
|
||||
supabaseMock: {
|
||||
from: vi.fn()
|
||||
},
|
||||
fetchTradeProfilesMock: vi.fn(),
|
||||
canonicalLifecycleHookState: {
|
||||
snapshot: null as any,
|
||||
loading: false,
|
||||
@ -39,8 +23,8 @@ vi.mock('../components/AuthContext', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../lib/supabaseClient', () => ({
|
||||
supabase: supabaseMock
|
||||
vi.mock('../lib/profileApi', () => ({
|
||||
fetchTradeProfiles: fetchTradeProfilesMock
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useCanonicalLifecycle', () => ({
|
||||
@ -70,13 +54,9 @@ describe('OverviewTab coverage maximization', () => {
|
||||
profileSignals: { 'p1': { signal: 'BUY', passed: true } }
|
||||
}
|
||||
};
|
||||
|
||||
supabaseMock.from.mockImplementation((table: string) => {
|
||||
if (table === tableNameProfiles) return createMockQuery([
|
||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||
]);
|
||||
return createMockQuery([]);
|
||||
});
|
||||
fetchTradeProfilesMock.mockResolvedValue([
|
||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||
]);
|
||||
});
|
||||
|
||||
it('covers all uptime and duration formatting paths', async () => {
|
||||
@ -99,7 +79,7 @@ describe('OverviewTab coverage maximization', () => {
|
||||
});
|
||||
|
||||
it('covers fallback capital logic', async () => {
|
||||
supabaseMock.from.mockImplementation(() => createMockQuery([]));
|
||||
fetchTradeProfilesMock.mockResolvedValue([]);
|
||||
mockBotState.settings.totalCapital = 25000;
|
||||
render(<OverviewTab botState={mockBotState} />);
|
||||
// The label in bot-status-bar is "Allocated:"
|
||||
@ -107,22 +87,16 @@ describe('OverviewTab coverage maximization', () => {
|
||||
});
|
||||
|
||||
it('covers Signal Active branches', async () => {
|
||||
supabaseMock.from.mockImplementation((table: string) => {
|
||||
if (table === tableNameProfiles) return createMockQuery([
|
||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 0, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||
]);
|
||||
return createMockQuery([]);
|
||||
});
|
||||
fetchTradeProfilesMock.mockResolvedValueOnce([
|
||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 0, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||
]);
|
||||
const { unmount } = render(<OverviewTab botState={mockBotState} />);
|
||||
await waitFor(() => expect(screen.getByText(/Signal active, no allocation/i)).toBeInTheDocument());
|
||||
unmount();
|
||||
|
||||
supabaseMock.from.mockImplementation((table: string) => {
|
||||
if (table === tableNameProfiles) return createMockQuery([
|
||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||
]);
|
||||
return createMockQuery([]);
|
||||
});
|
||||
fetchTradeProfilesMock.mockResolvedValueOnce([
|
||||
{ id: 'p1', name: 'Alpha', is_active: true, allocated_capital: 5000, strategy_config: { execution: { cooldownMinutes: 5 } } }
|
||||
]);
|
||||
render(<OverviewTab botState={mockBotState} />);
|
||||
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 () => {
|
||||
authMock.profile.role = 'admin';
|
||||
render(<OverviewTab botState={mockBotState} />);
|
||||
await waitFor(() => expect(supabaseMock.from).toHaveBeenCalled());
|
||||
await waitFor(() => expect(fetchTradeProfilesMock).toHaveBeenCalledWith({ scope: 'all' }));
|
||||
});
|
||||
|
||||
it('covers error handling in fetchData', async () => {
|
||||
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} />);
|
||||
await waitFor(() => expect(consoleSpy).toHaveBeenCalled());
|
||||
consoleSpy.mockRestore();
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
import { supabase } from '../lib/supabaseClient';
|
||||
import { tableNameProfiles } from '../lib/const';
|
||||
import { useAuth } from '../components/AuthContext';
|
||||
import { aggregateCanonicalLifecycleTrades } from '../lib/orderLifecycleLedger';
|
||||
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||
|
||||
interface OverviewTabProps {
|
||||
botState: BotState;
|
||||
@ -133,19 +132,7 @@ export const OverviewTab = ({ botState, previewAsCustomer = false, connected = t
|
||||
if (cancelled) return;
|
||||
|
||||
try {
|
||||
let profilesQuery = supabase
|
||||
.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 profilesData = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
|
||||
|
||||
const activeProfileRows: ActiveProfileCapital[] = ((profilesData as any[]) || [])
|
||||
.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 { PositionsTab } from './PositionsTab';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
import { tableNameOrders, tableNameProfiles, tableNameStocks } from '../lib/const';
|
||||
import { tableNameOrders, tableNameStocks } from '../lib/const';
|
||||
|
||||
const {
|
||||
authState,
|
||||
@ -13,7 +13,7 @@ const {
|
||||
entriesEqMock,
|
||||
ordersEqMock,
|
||||
historyEqMock,
|
||||
profilesEqMock
|
||||
fetchTradeProfilesMock
|
||||
} = vi.hoisted(() => ({
|
||||
authState: {
|
||||
user: { id: 'user-1' } as any,
|
||||
@ -24,7 +24,7 @@ const {
|
||||
entriesEqMock: vi.fn(),
|
||||
ordersEqMock: vi.fn(),
|
||||
historyEqMock: vi.fn(),
|
||||
profilesEqMock: vi.fn()
|
||||
fetchTradeProfilesMock: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('../components/AuthContext', () => ({
|
||||
@ -40,12 +40,16 @@ vi.mock('../lib/supabaseClient', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../lib/profileApi', () => ({
|
||||
fetchTradeProfiles: fetchTradeProfilesMock
|
||||
}));
|
||||
|
||||
interface QueryResult {
|
||||
data: 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 builder: any = {
|
||||
@ -65,8 +69,7 @@ const configureQueries = (plan: Record<TableKey, QueryResult[]>) => {
|
||||
const queues = {
|
||||
entries: [...plan.entries],
|
||||
orders: [...plan.orders],
|
||||
trade_history: [...plan.trade_history],
|
||||
trade_profiles: [...plan.trade_profiles]
|
||||
trade_history: [...plan.trade_history]
|
||||
};
|
||||
|
||||
fromMock.mockImplementation((table: string) => {
|
||||
@ -79,9 +82,6 @@ const configureQueries = (plan: Record<TableKey, QueryResult[]>) => {
|
||||
if (table === 'trade_history') {
|
||||
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());
|
||||
});
|
||||
};
|
||||
@ -262,7 +262,11 @@ describe('PositionsTab DOM behavior', () => {
|
||||
entriesEqMock.mockReset();
|
||||
ordersEqMock.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('alert', vi.fn());
|
||||
@ -361,13 +365,6 @@ describe('PositionsTab DOM behavior', () => {
|
||||
trade_history: [{
|
||||
data: [{ trade_id: 'TRD-BOT-3', profile_id: 'p2' }, { trade_id: '', profile_id: 'p1' }],
|
||||
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
|
||||
}],
|
||||
trade_history: [{ data: [], error: null }],
|
||||
trade_profiles: [{ data: [{ id: 'p1', name: 'High Risk Scalper' }], error: null }]
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
@ -507,9 +503,9 @@ describe('PositionsTab DOM behavior', () => {
|
||||
{ data: null, error: { message: 'v2 fallback failed' } },
|
||||
{ data: null, error: { message: 'legacy failed' } }
|
||||
],
|
||||
trade_history: [{ data: null, error: { message: 'history failed' } }],
|
||||
trade_profiles: [{ data: null, error: { message: 'profiles failed' } }]
|
||||
trade_history: [{ data: null, error: { message: 'history failed' } }]
|
||||
});
|
||||
fetchTradeProfilesMock.mockRejectedValueOnce(new Error('profiles failed'));
|
||||
|
||||
render(<PositionsTab botState={{
|
||||
symbols: {},
|
||||
@ -536,7 +532,7 @@ describe('PositionsTab DOM behavior', () => {
|
||||
expect(entriesEqMock).toHaveBeenCalledWith('user_id', 'user-2');
|
||||
expect(ordersEqMock).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(
|
||||
'[PositionsTab] V3 order query failed, falling back to v2 columns:',
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
import { supabase } from '../lib/supabaseClient';
|
||||
import { tradingRuntime } from '../lib/runtime';
|
||||
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 { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
|
||||
import { fetchTradeProfiles } from '../lib/profileApi';
|
||||
|
||||
interface PositionsTabProps {
|
||||
botState: BotState;
|
||||
@ -523,16 +524,17 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
|
||||
console.warn('[PositionsTab] History trade lookup failed:', histError.message);
|
||||
}
|
||||
|
||||
let profilesQuery = supabase
|
||||
.from(tableNameProfiles)
|
||||
.select('id, name')
|
||||
.order('name', { ascending: true });
|
||||
|
||||
if (profile?.role !== 'admin') {
|
||||
profilesQuery = profilesQuery.eq('user_id', user.id);
|
||||
}
|
||||
|
||||
const { data: profData, error: profError } = await profilesQuery;
|
||||
let profData: Array<{ id: string; name: string }> = [];
|
||||
let profError: Error | null = null;
|
||||
try {
|
||||
const profiles = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' });
|
||||
profData = profiles.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: String(item.name || item.id || 'Unnamed Profile')
|
||||
}));
|
||||
} catch (error) {
|
||||
profError = error as Error;
|
||||
}
|
||||
|
||||
if (posError) {
|
||||
console.error('[PositionsTab] Failed loading manual positions:', posError.message);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user