diff --git a/docs/design/pages/apiClient.js b/docs/design/pages/apiClient.js index d2840c3..a9527fd 100644 --- a/docs/design/pages/apiClient.js +++ b/docs/design/pages/apiClient.js @@ -6,7 +6,6 @@ Configuration (set before this script loads): window.API_BASE = 'http://localhost:8000' (default) window.API_KEY = 'your-key' (optional) - window.USE_MOCK_DATA = true (fallback to CT.*) Request/response shapes are generated from the API's OpenAPI schema into ./openapi.d.ts (`npm run gen:types`). CI fails if either drifts (P3-4). @@ -394,7 +393,7 @@ window.CT_GROUPS = CT_GROUPS; ============================================================ */ const CT_PREFS = (() => { const DEFAULTS = { locale: 'pt-BR', timezone: 'auto', number_locale: 'auto', date_locale: 'auto' }; - let current = { ...DEFAULTS, ...(window.MOCK_PREFS ?? {}) }; + let current = { ...DEFAULTS }; const emit = () => window.dispatchEvent(new CustomEvent('ct:prefs', { detail: current })); return { get: () => current, @@ -422,8 +421,8 @@ window.CT_PREFS = CT_PREFS; Global auth/session store (A1). Mirrors CT_PAIR's pattern. kind: 'off' (auth disabled — no auth UI), 'user' (real session), 'demo' (public demo, read-only), 'anonymous' (must log in). - Mock branch (e2e): window.MOCK_AUTH='none' boots unauthenticated; - anything else auto-authenticates as CT.currentUser (role via MOCK_ROLE). + Permissions ALWAYS come from GET /v1/auth/me (fromMe); e2e serve o probe + via o fixture page.route (installMockApi) — sem branch de mock aqui. ============================================================ */ const CT_AUTH = (() => { let current = { @@ -448,15 +447,6 @@ const CT_AUTH = (() => { }; }; - // Mirror of the backend matrix (src/auth/rbac.py) for mock/e2e mode only — - // in live mode permissions ALWAYS come from /v1/auth/me. - const MOCK_MATRIX = { - visualizador: [], - operador: ['approve_order', 'change_autonomy', 'view_audit'], - admin: ['approve_order', 'change_autonomy', 'change_risk', 'edit_settings', - 'manage_keys', 'view_audit', 'manage_users'], - }; - return { state: () => current, kind: () => current.kind, @@ -468,24 +458,11 @@ const CT_AUTH = (() => { return false; }, load: async () => { - if (window.USE_MOCK_DATA) { - const none = window.MOCK_AUTH === 'none'; - const role = window.MOCK_ROLE ?? window.CT?.currentUser?.role ?? 'admin'; - current = none - ? { loaded: true, mode: 'required', kind: 'anonymous', - authenticated: false, user: null, permissions: [] } - : { loaded: true, mode: 'mock', - kind: window.MOCK_AUTH === 'demo' ? 'demo' : 'user', - authenticated: window.MOCK_AUTH !== 'demo', - user: { ...(window.CT?.currentUser ?? { name: 'Demo', email: 'demo@dev' }), role }, - permissions: window.MOCK_AUTH === 'demo' ? [] : (MOCK_MATRIX[role] ?? []) }; - } else { - try { - current = fromMe(await CT_API.getMe()); - } catch (_) { - current = { loaded: true, mode: 'unreachable', kind: 'off', - authenticated: false, user: null, permissions: [] }; - } + try { + current = fromMe(await CT_API.getMe()); + } catch (_) { + current = { loaded: true, mode: 'unreachable', kind: 'off', + authenticated: false, user: null, permissions: [] }; } emit(); return current; diff --git a/docs/design/pages/app.jsx b/docs/design/pages/app.jsx index dbcddb3..18e3ad9 100644 --- a/docs/design/pages/app.jsx +++ b/docs/design/pages/app.jsx @@ -76,7 +76,7 @@ class GlobalBoundary extends React.Component { // ---- Alert Drawer ---- function AlertDrawer({ onClose }) { - const [alerts, setAlerts] = useState(CT.alerts ?? []); + const [alerts, setAlerts] = useState([]); // populado pelo stream SSE /v1/alerts const esRef = useRef(null); useEffect(() => { @@ -179,10 +179,10 @@ function App() { }; const [screen, setScreen] = useState(getInitialScreen); - const [pendingCount, setPendingCount] = useState(CT.pendingOrders?.length ?? 0); + const [pendingCount, setPendingCount] = useState(0); // poll de GET /v1/orders const [showAlerts, setShowAlerts] = useState(false); const [showTweaks, setShowTweaks] = useState(false); - const [alertCount, setAlertCount] = useState(CT.alerts?.length ?? 0); + const [alertCount, setAlertCount] = useState(0); // stream SSE /v1/alerts const [toasts, setToasts] = useState([]); // A1 auth gate: 'loading' → probe /v1/auth/me; 'login' → auth screens instead // of the shell; 'ready' → shell (kind 'off'/'user'/'demo'). Locked = overlay. @@ -205,8 +205,8 @@ function App() { useEffect(() => CT_PREFS.subscribe(() => setPrefsRev(n => n + 1)), []); // A10: first admin login with an incomplete guide opens it — ONCE per boot, - // never hijacking an explicit deep link. Mock default is "completed" so the - // e2e suite boots unchanged; MOCK_ONBOARDING='pending' opts in. + // never hijacking an explicit deep link. Real: GET /v1/onboarding/status; o e2e + // serve completed por padrão (boot inalterado) e o cenário pending pelo fixture. const initialHashRef = useRef(window.location.hash.replace('#', '')); useEffect(() => { if (!booted || auth.kind !== 'user' || auth.user?.role !== 'admin') return; @@ -215,10 +215,6 @@ function App() { const open = (st) => { if (st && !st.completed && !st.dismissed) navigate('onboarding'); }; - if (window.USE_MOCK_DATA) { - if (window.MOCK_ONBOARDING === 'pending') open({ completed: false, dismissed: false }); - return; - } CT_API.getOnboarding().then(open).catch(() => {}); }, [booted, auth.kind]); @@ -232,11 +228,11 @@ function App() { // Only a bare boot (empty hash) is a candidate — an explicit #overview (or any // deep link) is respected, never hijacked. if (initialHashRef.current) return; - const onboardingWins = window.USE_MOCK_DATA - ? (window.MOCK_ONBOARDING === 'pending' && auth.kind === 'user' && auth.user?.role === 'admin') - : false; // real onboarding runs in its own effect; the hash guard below yields - if (onboardingWins) return; deskLandingRef.current = true; + // Determinístico vs. o onboarding (que roda no effect acima): relemos o hash na + // resolução — se o onboarding já navegou (#onboarding), a Mesa cede; o auto-open + // não tem guarda de hash e sobrescreve se a Mesa tiver ido primeiro. Ambas as + // ordens de resolução terminam em #onboarding quando o guia está incompleto. loadPairsRich().then((r) => { const operados = (r && r.operados) || []; const h = window.location.hash; diff --git a/docs/design/pages/build.mjs b/docs/design/pages/build.mjs index 645d3b1..a6be2d1 100644 --- a/docs/design/pages/build.mjs +++ b/docs/design/pages/build.mjs @@ -41,7 +41,36 @@ await mkdir(vendor, { recursive: true }); // arquivos (componentes, telas, CT, CT_API) sao expostos via window.* dentro // de cada arquivo, entao isolar os locais nao quebra as refs bare (). const jsx = (await readdir(here)).filter((f) => f.endsWith(".jsx")).sort(); -const js = ["apiClient.js", "data.js"].filter((f) => existsSync(path.join(here, f))); +const js = ["apiClient.js"].filter((f) => existsSync(path.join(here, f))); + +// 2b. Guard anti-regressão de mocks (limpeza de mocks, fatia 5.7): a PRODUÇÃO nunca +// pode referenciar o objeto mock global (CT.*), as flags USE_MOCK_DATA/MOCK_*, +// nem o data.js deletado — o mock vive SÓ em e2e/fixtures (fora de readdir(here), +// que só lê os fontes top-level, não a pasta e2e/). Falha o build e o CI +// (console-build) apontando a diretriz. O seam de teste do boundary usa +// window.__E2E_THROW_SCREEN (sem prefixo MOCK_/CT.), então não é pego aqui. +const MOCK_GUARD = [ + [/\bUSE_MOCK_DATA\b/, "USE_MOCK_DATA"], + [/\bMOCK_[A-Z][A-Z0-9_]*/, "MOCK_* flag"], + [/\bCT\.[a-zA-Z]/, "CT.* mock global — use os stores reais CT_API/CT_AUTH/CT_PAIR/CT_PREFS"], + [/\bdata\.js\b/, "data.js (deletado)"], +]; +const guardHits = []; +for (const f of [...jsx, ...js]) { + (await readFile(path.join(here, f), "utf8")).split("\n").forEach((line, i) => { + for (const [re, label] of MOCK_GUARD) { + if (re.test(line)) guardHits.push(` ${f}:${i + 1} [${label}] ${line.trim()}`); + } + }); +} +if (guardHits.length) { + throw new Error( + "[mock-guard] referência a mock proibida na produção (o mock vive só em " + + "e2e/fixtures — limpeza de mocks 5.7):\n" + guardHits.join("\n"), + ); +} +log(`mock-guard OK: ${jsx.length + js.length} fontes sem USE_MOCK_DATA/MOCK_*/CT.*/data.js`); + log(`transpilando ${jsx.length} .jsx + ${js.length} .js (minify)`); await build({ entryPoints: [...jsx, ...js].map((f) => path.join(here, f)), diff --git a/docs/design/pages/components.jsx b/docs/design/pages/components.jsx index c9f82fd..e7acf86 100644 --- a/docs/design/pages/components.jsx +++ b/docs/design/pages/components.jsx @@ -372,9 +372,7 @@ window.DataState = DataState; let _pairsPromise = null; function loadPairs() { if (!_pairsPromise) { - _pairsPromise = window.USE_MOCK_DATA - ? Promise.resolve(['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']) - : CT_API.getPairs().catch(() => ['BTC/USDT']); + _pairsPromise = CT_API.getPairs().catch(() => ['BTC/USDT']); } return _pairsPromise; } @@ -382,28 +380,15 @@ window.loadPairs = loadPairs; // N1: rich pair source for the selector — operated (loop trades) vs observable // (allowlist, analysis-only). Kept separate from loadPairs() so Mercado/Backtest -// (which want a flat list) are untouched. Mock shows both groups + enough items -// to trigger search, so the demo mirrors the multi-asset reality. +// (which want a flat list) are untouched. let _pairsRichPromise = null; function loadPairsRich(force) { if (force) _pairsRichPromise = null; // N8²: re-fetch after add/remove if (!_pairsRichPromise) { - // e2e hook: window.MOCK_OPERATED (array of symbols) overrides the operated - // set, so a spec can force single-pair (landing stays Visão Geral) vs multi. - const mockOperated = (typeof window !== 'undefined' && window.MOCK_OPERATED) || null; - _pairsRichPromise = window.USE_MOCK_DATA - ? Promise.resolve({ - operados: (mockOperated || ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']) - .map((s, i) => ({ symbol: s, status: i === 4 ? 'aguardando' : 'operando', - paused: s === 'BNB/USDT', // N9: one paused pair for the demo/screenshots - last_cycle_at: i === 4 ? null : new Date().toISOString() })), - observaveis: ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT', - 'ADA/USDT', 'DOGE/USDT', 'AVAX/USDT'], - }) - : CT_API.getPairsRich().catch(() => ({ - operados: [{ symbol: 'BTC/USDT', status: 'aguardando', last_cycle_at: null }], - observaveis: ['BTC/USDT'], - })); + _pairsRichPromise = CT_API.getPairsRich().catch(() => ({ + operados: [{ symbol: 'BTC/USDT', status: 'aguardando', last_cycle_at: null }], + observaveis: ['BTC/USDT'], + })); } return _pairsRichPromise; } diff --git a/docs/design/pages/data.js b/docs/design/pages/data.js deleted file mode 100644 index 1e08f87..0000000 --- a/docs/design/pages/data.js +++ /dev/null @@ -1,542 +0,0 @@ -/* ============================================================ - Criptotrade — mock data (deterministic) - Attaches CT.* to window. Plain JS (no JSX). - ============================================================ */ -(function () { - // ---- seeded RNG (mulberry32) ---- - function rng(seed) { - let a = seed >>> 0; - return function () { - a |= 0; a = (a + 0x6D2B79F5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; - } - - // ---- OHLCV candle series (BTC/USDT-ish around 67k) ---- - function makeCandles(n, seed, base) { - const r = rng(seed); - const out = []; - let price = base; - for (let i = 0; i < n; i++) { - const drift = Math.sin(i / 9) * 140 + Math.sin(i / 23) * 320; - const vol = 180 + r() * 260; - const open = price; - const move = (r() - 0.46) * vol + drift * 0.06; - let close = open + move; - const high = Math.max(open, close) + r() * vol * 0.6; - const low = Math.min(open, close) - r() * vol * 0.6; - const volume = 40 + r() * 120 + (Math.abs(move) / vol) * 80; - out.push({ i, open, close, high, low, volume }); - price = close; - } - return out; - } - - const candles = makeCandles(70, 7, 65200); - const last = candles[candles.length - 1]; - const price = last.close; - - // bollinger bands over candles (period 20) - function bollinger(data, p = 20, k = 2) { - return data.map((c, idx) => { - if (idx < p - 1) return { i: c.i, mid: null, up: null, low: null }; - const slice = data.slice(idx - p + 1, idx + 1).map(x => x.close); - const mean = slice.reduce((a, b) => a + b, 0) / p; - const sd = Math.sqrt(slice.reduce((a, b) => a + (b - mean) ** 2, 0) / p); - return { i: c.i, mid: mean, up: mean + k * sd, low: mean - k * sd }; - }); - } - const bb = bollinger(candles); - - // RSI line - function makeLine(n, seed, lo, hi, smooth) { - const r = rng(seed); const out = []; let v = (lo + hi) / 2; - for (let i = 0; i < n; i++) { v += (r() - 0.5) * (hi - lo) / smooth; v = Math.max(lo, Math.min(hi, v)); out.push(v); } - return out; - } - - const CT = {}; - CT.rng = rng; - CT.makeLine = makeLine; - - CT.symbol = { - pair: 'BTC/USDT', price: price, change24h: 2.34, changeUsd: 1532.40, - high24h: price + 980, low24h: price - 1420, volume24h: '1.84B', - }; - CT.candles = candles; - CT.bb = bb; - - // ---- regime ---- - CT.regime = { - current: 'sideways', // sideways | strong_uptrend | strong_downtrend | chaotic - confidence: 0.71, - label: 'Lateral', strategy: 'Grid', - options: [ - { key: 'sideways', label: 'Lateral', strat: 'Grid', desc: 'Mercado sem tendência definida' }, - { key: 'strong_uptrend', label: 'Alta forte', strat: 'DCA', desc: 'Tendência de alta consistente' }, - { key: 'strong_downtrend', label: 'Baixa forte', strat: '— (sem long)', desc: 'Tendência de baixa' }, - { key: 'chaotic', label: 'Caótico', strat: '— (sem trade)', desc: 'Volatilidade extrema' }, - ], - extreme: null, // 'EUFORIA' | 'PANICO' | null - }; - - // ---- indicators ---- - CT.indicators = { - rsi: 48.6, - rsiSeries: [42,45,51,55,49,46,44,47,52,56,53,49,47,45,48.6], - macd: { macd: 38.2, signal: 52.7, hist: -14.5 }, - macdHist: [22,18,9,-4,-12,-19,-22,-17,-9,-3,-8,-13,-16,-14.5], - stoch: { k: 31.4, d: 38.9 }, - bollPctB: 0.38, - atr: 612.40, - atrPctOfPrice: 0.92, - ema9: price - 120, ema21: price - 60, - sma20: price + 40, sma50: price + 380, sma200: price - 2100, - obv: 'distribuição', obvTrend: -1, - volumeRatio: 0.84, - }; - - // ---- support / resistance ---- - CT.sr = { - resistance: [ - { price: price + 1850, strength: 4, touches: 4 }, - { price: price + 920, strength: 3, touches: 3 }, - ], - support: [ - { price: price - 760, strength: 5, touches: 5 }, - { price: price - 1640, strength: 2, touches: 2 }, - ], - fib: [ - { level: 0, price: price + 1850 }, - { level: 23.6, price: price + 1180 }, - { level: 38.2, price: price + 720 }, - { level: 50, price: price + 130 }, - { level: 61.8, price: price - 470 }, - { level: 78.6, price: price - 1180 }, - { level: 100, price: price - 1900 }, - ], - }; - - // ---- volume profile ---- - CT.volumeProfile = (function () { - const r = rng(31); const bins = 22; const out = []; - const lo = price - 2400, hi = price + 2200; - for (let i = 0; i < bins; i++) { - const p = lo + (hi - lo) * (i / (bins - 1)); - const dist = Math.exp(-Math.pow((p - (price - 200)) / 900, 2)); - out.push({ price: p, vol: dist * 100 + r() * 18 }); - } - const maxBin = out.reduce((m, b) => (b.vol > m.vol ? b : m), out[0]); - return { - bins: out, - poc: maxBin.price, - vah: price + 540, - val: price - 980, - lvn: [price + 1200, price - 1850], - }; - })(); - - // ---- detected patterns ---- - CT.patterns = [ - { name: 'Retângulo (consolidação)', dir: 'neutral', confidence: 0.78, target: price + 60, note: 'Faixa lateral entre suporte e resistência' }, - { name: 'Triângulo Ascendente', dir: 'up', confidence: 0.64, target: price + 1620, note: 'Topos planos, fundos ascendentes' }, - { name: 'Double Bottom', dir: 'up', confidence: 0.52, target: price + 980, note: 'Dois fundos próximos a $' + Math.round(price - 760).toLocaleString() }, - ]; - - // ---- current signal ---- - CT.signal = { - action: 'buy', strategy: 'grid', confidence: 0.71, - entry: price - 40, stop: price - 760, takeProfit: price + 920, sizePct: 2.4, - rr: 2.6, notional: 1240, at: '14:32:08', - }; - - // ---- multi-timeframe confluence ---- - CT.confluence = { - aligned: false, - direction: null, - timeframes: [ - { tf: '1h', trend: 'bullish', rsi: 48.6, macd_hist: -14.5, regime: 'sideways', rsi_divergence: null, macd_divergence: null }, - { tf: '4h', trend: 'bullish', rsi: 55.2, macd_hist: 8.1, regime: 'strong_uptrend', rsi_divergence: null, macd_divergence: 'bullish_divergence' }, - { tf: '1d', trend: 'bearish', rsi: 61.0, macd_hist: 3.2, regime: 'strong_uptrend', rsi_divergence: 'bearish_divergence', macd_divergence: null }, - ], - }; - - // ---- confidence breakdown (5 factors) ---- - CT.confidenceBreakdown = [ - { key: 'Alinhamento de tendência', weight: 0.25, score: 0.58 }, - { key: 'Confluência de indicadores', weight: 0.30, score: 0.74 }, - { key: 'Proximidade de S/R', weight: 0.20, score: 0.81 }, - { key: 'Confirmação de volume', weight: 0.15, score: 0.62 }, - { key: 'Divergência RSI/MACD', weight: 0.10, score: 0.88 }, - ]; - - // ---- capital / risk ---- - CT.capital = { - initial: 10000, - value: 11842.60, - pnlPct: 18.43, - exposurePct: 14.2, - openPositions: 2, - }; - - CT.drawdown = { - daily: { value: -1.2, limit: -3, status: 'ok', action: 'Pausa trading pelo dia' }, - weekly: { value: -3.4, limit: -6, status: 'warn', action: 'Reduz posições à metade' }, - monthly: { value: -7.1, limit: -15, status: 'ok', action: 'Suspende e exige revisão' }, - overallStatus: 'warn', // ok | warn | paused_daily | reduced_weekly | suspended_monthly - }; - - // equity curve + drawdown series - CT.equity = (function () { - const r = rng(11); const out = []; let eq = 10000; let peak = 10000; - for (let i = 0; i < 90; i++) { - eq += (r() - 0.42) * 130 + Math.sin(i / 12) * 22; - eq = Math.max(9200, eq); - peak = Math.max(peak, eq); - out.push({ i, equity: eq, dd: (eq - peak) / peak * 100 }); - } - out[out.length - 1].equity = 11842.6; - return out; - })(); - - CT.circuitBreaker = { - status: 'closed', // closed | open - triggers: [ - { key: 'Perda diária ≥ -4%', value: -1.8, limit: -4, hit: false }, - { key: '3 perdas consecutivas', value: 1, limit: 3, hit: false }, - ], - cooldownHours: 24, cooldownRemaining: 0, - }; - - CT.kelly = { - winRate: 0.586, avgWinPct: 2.84, avgLossPct: 1.62, - fullKelly: 0.331, fraction: 0.25, fractionalKelly: 0.083, - riskOfRuin: 2.4, // % - trades: 142, - }; - - CT.riskConfig = { - kellyFraction: 0.25, - minPositionPct: 0.5, - maxPositionPct: 5, - ddDaily: 3, ddWeekly: 6, ddMonthly: 15, - }; - - // ---- guardrails (Risk Agent) ---- - CT.guardrails = [ - { key: 'Volatilidade (ATR/BB)', limit: '≤ 10%', value: '6.2%', ok: true }, - { key: 'Liquidez (volume ratio)', limit: '≥ 0.3', value: '0.84', ok: true }, - { key: 'Tamanho de posição', limit: '≤ 5%', value: '2.4%', ok: true }, - { key: 'Stop loss obrigatório', limit: 'presente', value: 'definido', ok: true }, - { key: 'Risk/reward', limit: '≥ 2.5×', value: '2.6×', ok: true }, - ]; - - // ---- behavioral guard ---- - CT.behavioral = [ - { key: 'Revenge trading', desc: 'Tamanho 50%+ maior após 2 perdas', status: 'ok', detail: 'Sem padrão detectado' }, - { key: 'Euforia', desc: 'Tamanho 20%+ maior após 3 vitórias', status: 'warn', detail: 'Último trade +18% acima da média', action: 'Forçar Kelly half' }, - { key: 'Overconfidence', desc: 'Confiança acima do win rate real', status: 'ok', detail: 'Confiança 71% · win rate 59%' }, - ]; - - // ---- agents ---- - CT.agents = [ - { id: 'strategy', name: 'Strategy Agent', domain: 'trading', status: 'active', implemented: true, cycles: 142, last: '14:32:08', desc: 'Detecta regime e gera sinais' }, - { id: 'risk', name: 'Risk Agent', domain: 'security', status: 'active', implemented: true, cycles: 142, last: '14:32:09', desc: 'Valida guardrails e proteções' }, - { id: 'execution', name: 'Execution Agent', domain: 'trading', status: 'idle', implemented: true, cycles: 38, last: '14:30:51', desc: 'Executa ordens (paper)' }, - { id: 'behavioral', name: 'Behavioral Guard', domain: 'security', status: 'active', implemented: true, cycles: 142, last: '14:32:09', desc: 'Detecta vieses comportamentais' }, - { id: 'orchestrator', name: 'Orchestrator', domain: 'orchestration', status: 'active', implemented: true, cycles: 142, last: '14:32:10', desc: 'Coordena o pipeline de ciclo' }, - { id: 'auditor', name: 'Auditor Agent', domain: 'security', status: 'not_implemented', implemented: false, cycles: 0, last: null, desc: 'Auditoria de conformidade' }, - ]; - - // ---- strategies config ---- - CT.strategies = { - grid: { levels: 10, spacingPct: 1.0, allocPct: 10, regime: 'sideways' }, - dca: { entries: 3, spacingPct: 1.5, stopPct: 8, rsiOversold: 35, regime: 'sideways, strong_uptrend' }, - meanReversion: { rsiOversold: 30, rsiOverbought: 70, atrMult: 2.0, minRR: 2.0 }, - }; - - // ---- orders ---- - const _ords = []; - const pairs = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT']; - const str016 = ['grid', 'dca', 'mean_reversion']; - const statuses = ['pending', 'pending', 'approved', 'filled', 'filled', 'filled', 'rejected', 'cancelled', 'filled', 'filled', 'filled', 'filled']; - const ro = rng(99); - for (let i = 0; i < 22; i++) { - const side = ro() > 0.42 ? 'buy' : 'sell'; - let pair = pairs[Math.floor(ro() * pairs.length)]; - if (i < 2) pair = pairs[i]; // N4: the 2 pending orders span 2 pairs so the filter chips show - const px = pair === 'BTC/USDT' ? price : pair === 'ETH/USDT' ? 3420 : 168; - const qty = +(pair === 'BTC/USDT' ? (0.01 + ro() * 0.04) : pair === 'ETH/USDT' ? (0.2 + ro() * 0.8) : (3 + ro() * 12)).toFixed(4); - const st = i < 2 ? 'pending' : statuses[i % statuses.length]; - const conf = +(0.5 + ro() * 0.42).toFixed(2); - const sizePct = +(0.8 + ro() * 3.6).toFixed(1); - const notional = +(px * qty).toFixed(2); - const hh = 14 - Math.floor(i / 3); const mm = (50 - i * 4 + 60) % 60; - _ords.push({ - id: 'ord_' + (3480 - i), - pair, side, quantity: qty, price: px, notional, - status: st, strategy: str016[Math.floor(ro() * 3)], agent_id: 'strategy', - confidence: conf, sizePct, - stop: side === 'buy' ? +(px * 0.97).toFixed(2) : +(px * 1.03).toFixed(2), - takeProfit: side === 'buy' ? +(px * 1.08).toFixed(2) : +(px * 0.92).toFixed(2), - rr: +(2.4 + ro() * 0.8).toFixed(1), - reason: side === 'buy' - ? 'RSI saindo de sobrevendido + suporte forte testado; confluência de volume confirma entrada.' - : 'Resistência rejeitada com divergência de MACD; realização parcial recomendada.', - auto_approved: st === 'filled' && conf > 0.7, - critical: i === 1, - operator: (st === 'approved' || st === 'rejected') ? 'operador' : null, - operatorNote: st === 'rejected' ? 'Volatilidade alta antes de evento macro.' : null, - created_at: `2026-06-08 ${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:0${i % 10}`, - }); - } - CT.orders = _ords; - CT.pendingOrders = _ords.filter(o => o.status === 'pending'); - - // ---- HITL config ---- - CT.hitl = { - level: 2, threshold: 1000, - pending: CT.pendingOrders.length, approvedToday: 7, rejectedToday: 2, - decisionTimeout: 300, - levels: [ - { level: 0, threshold: 0, desc: 'Manual total — toda ordem exige aprovação' }, - { level: 1, threshold: 500, desc: 'Semiautônomo baixo' }, - { level: 2, threshold: 1000, desc: 'Semiautônomo médio' }, - { level: 3, threshold: 5000, desc: 'Semiautônomo alto (críticas ainda exigem humano)' }, - ], - }; - - // ---- journal ---- - const jr = rng(55); - const setups = ['Grid · suporte', 'DCA · pullback', 'Mean reversion · RSI<30', 'Breakout triângulo', 'Double bottom']; - const _journal = []; - for (let i = 0; i < 16; i++) { - const before = 1 + Math.floor(jr() * 10); - const followed = jr() > 0.34; - const pnl = +(((followed ? 1 : -1) * (jr() * 4) + (before > 6 ? -0.6 : 0.4))).toFixed(2); - const after = Math.max(1, Math.min(10, Math.round(before + pnl))); - _journal.push({ - id: i, date: `06/${String(8 - Math.floor(i / 3)).padStart(2, '0')}`, - setup: setups[Math.floor(jr() * setups.length)], - before, after, stopDefined: jr() > 0.15, followed, - pnl, note: followed ? 'Plano seguido' : 'Desviou do plano', - }); - } - CT.journal = _journal; - CT.journalMetrics = { - byEmotion: [ - { band: '1–3', label: 'Baixo', winRate: 0.71, trades: 5 }, - { band: '4–6', label: 'Neutro', winRate: 0.62, trades: 7 }, - { band: '7–10', label: 'Alto', winRate: 0.38, trades: 4 }, - ], - planFollowedPnl: 2.14, - planDeviatedPnl: -1.38, - disciplineCorrelation: 0.67, - realWinRate: 0.586, - }; - // scatter emotion x pnl - CT.journalScatter = _journal.map(j => ({ x: j.before, y: j.pnl, followed: j.followed })); - // heatmap day x hour - CT.heatmap = (function () { - const days = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom']; - const hr = rng(77); const out = []; - for (let d = 0; d < 7; d++) for (let h = 0; h < 12; h++) { - out.push({ day: d, dayLabel: days[d], hour: 8 + h, winRate: hr() }); - } - return { days, data: out }; - })(); - - // ---- backtest ---- - CT.backtest = { - totalTrades: 142, winRate: 0.586, pnlUsd: 1842.6, pnlPct: 18.43, - maxDrawdown: -7.1, sharpe: 1.84, profitFactor: 2.12, - avgWin: 2.84, avgLoss: -1.62, expectancy: 0.94, - equity: CT.equity, - }; - CT.monteCarlo = (function () { - const r = rng(123); const sims = []; const N = 1000; - for (let i = 0; i < N; i++) { - let v = 0; for (let t = 0; t < 142; t++) v += (r() - 0.41) * 1.6; - sims.push(v * 10 + 1840); - } - sims.sort((a, b) => a - b); - const pct = q => sims[Math.floor(q * N)]; - // histogram - const lo = sims[0], hi = sims[N - 1]; const bins = 28; const hist = new Array(bins).fill(0); - sims.forEach(s => { let b = Math.floor((s - lo) / (hi - lo) * (bins - 1)); hist[b]++; }); - return { - n: N, - p5: pct(0.05), p50: pct(0.5), p95: pct(0.95), - profitablePct: sims.filter(s => s > 0).length / N, - rejected: pct(0.05) < 0, - hist, lo, hi, bins, - }; - })(); - CT.walkForward = { - valid: true, windows: 6, windowSize: 252, - sharpeDeviation: 0.18, threshold: 0.30, - folds: [ - { fold: 1, trainSharpe: 1.92, testSharpe: 1.71 }, - { fold: 2, trainSharpe: 1.84, testSharpe: 1.62 }, - { fold: 3, trainSharpe: 2.01, testSharpe: 1.88 }, - { fold: 4, trainSharpe: 1.76, testSharpe: 1.59 }, - { fold: 5, trainSharpe: 1.95, testSharpe: 1.74 }, - { fold: 6, trainSharpe: 1.88, testSharpe: 1.81 }, - ], - }; - CT.backtestConfig = { - initialCapital: 10000, commissionPct: 0.1, slippageBps: 5, - walkForwardWindow: 252, monteCarloSims: 1000, - }; - - // ---- alerts ---- - CT.alerts = [ - { id: 'a1', severity: 'medium', type: 'behavioral', message: 'Euforia detectada — tamanho 18% acima da média após 3 vitórias.', agent_id: 'behavioral', pair: 'BTC/USDT', auto_action: 'Kelly half forçado', at: '14:28:41' }, - { id: 'a2', severity: 'low', type: 'guardrail', message: 'Risk/reward 2.6× aprovado (mínimo 2.5×).', agent_id: 'risk', pair: 'BTC/USDT', auto_action: null, at: '14:32:08' }, - { id: 'a3', severity: 'high', type: 'drawdown', message: 'Drawdown semanal em -3.4% (limite -6%) — atenção.', agent_id: 'risk', pair: null, auto_action: null, at: '13:02:11' }, - { id: 'a4', severity: 'low', type: 'regime', message: 'Regime confirmado como lateral — estratégia Grid ativa.', agent_id: 'strategy', pair: 'BTC/USDT', auto_action: 'Ativar Grid', at: '12:40:55' }, - { id: 'a5', severity: 'critical', type: 'guardrail', message: 'Ordem rejeitada: stop loss ausente em ordem manual.', agent_id: 'risk', pair: 'ETH/USDT', auto_action: 'Ordem bloqueada', at: '11:18:03' }, - { id: 'a6', severity: 'low', type: 'risk', message: 'Risco de ruína recalculado: 2.4% (alerta acima de 5%).', agent_id: 'risk', pair: null, auto_action: null, at: '10:55:22' }, - ]; - - CT.alertThresholds = { - revengeSize: 50, euphoriaSize: 20, overconfidenceGap: 15, riskOfRuin: 5, - }; - - // A1: mock identity for e2e/demo (CT_AUTH mock branch auto-authenticates as this). - CT.currentUser = { - id: 'mock', name: 'Operador Demo', email: 'demo@criptotrade.dev', role: 'admin', - job_title: 'Operador-chefe', avatar_color: 'info', - }; - - // A3: mock users/roles for the admin screen (mirrors src/auth/rbac.py). - CT.adminUsers = [ - { id: 'u1', name: 'Operador Demo', email: 'demo@criptotrade.dev', role: 'admin', - status: 'active', last_login_at: '2026-07-18T09:12:00+00:00' }, - { id: 'u2', name: 'Ana Operadora', email: 'ana@criptotrade.dev', role: 'operador', - status: 'active', last_login_at: '2026-07-17T18:40:00+00:00' }, - { id: 'invite:i1', name: null, email: 'novo@criptotrade.dev', role: 'visualizador', - status: 'pending', invite_id: 'i1', last_login_at: null }, - ]; - CT.roles = [ - { id: 'visualizador', label: 'Visualizador', permissions: [] }, - { id: 'operador', label: 'Operador', - permissions: ['approve_order', 'change_autonomy', 'view_audit'] }, - { id: 'admin', label: 'Admin', - permissions: ['approve_order', 'change_autonomy', 'change_risk', - 'edit_settings', 'manage_keys', 'view_audit', 'manage_users'] }, - ]; - - // A4: mock audit-trail events (mirrors the /v1/audit envelope). - CT.auditEvents = [ - { id: 8, ts: '2026-07-18T09:12:00+00:00', action: 'login', actor: 'demo@criptotrade.dev', - entity: 'demo@criptotrade.dev', ip: '187.20.14.2', ua: 'Chrome/Linux', success: true, - before: null, after: null, detail: null }, - { id: 7, ts: '2026-07-18T08:55:00+00:00', action: 'config_changed', actor: 'demo@criptotrade.dev', - entity: 'risk', ip: null, ua: null, success: null, - before: { max_daily_loss_pct: 5.0 }, after: { max_daily_loss_pct: 4.0 }, detail: null, - event_type: 'config_changed', - data: { actor: 'demo@criptotrade.dev', scope: 'risk', - before: { max_daily_loss_pct: 5.0 }, after: { max_daily_loss_pct: 4.0 } } }, - { id: 6, ts: '2026-07-18T08:40:00+00:00', action: 'autonomy_changed', actor: 'ana@criptotrade.dev', - entity: null, ip: null, ua: null, success: null, - before: { level: 1 }, after: { level: 2 }, detail: 'Mercado estável, subindo autonomia' }, - { id: 5, ts: '2026-07-18T08:10:00+00:00', action: 'order_approved', actor: 'ana@criptotrade.dev', - entity: 'BTC/USDT', ip: null, ua: null, success: null, - before: null, after: null, detail: null }, - { id: 4, ts: '2026-07-17T22:05:00+00:00', action: 'order_rejected', actor: 'ana@criptotrade.dev', - entity: 'ETH/USDT', ip: null, ua: null, success: null, - before: null, after: null, detail: null }, - { id: 3, ts: '2026-07-17T21:00:00+00:00', action: 'position_closed', actor: 'orchestrator', - entity: 'BTC/USDT', ip: null, ua: null, success: null, - before: null, after: null, detail: 'P&L +12.40 USDT' }, - { id: 2, ts: '2026-07-17T20:30:00+00:00', action: 'user_management', actor: 'demo@criptotrade.dev', - entity: 'novo@criptotrade.dev', ip: null, ua: null, success: true, - before: null, after: null, detail: 'novo@criptotrade.dev as visualizador' }, - { id: 1, ts: '2026-07-17T19:00:00+00:00', action: 'circuit_breaker', actor: 'orchestrator', - entity: null, ip: null, ua: null, success: null, - before: null, after: null, detail: '3 perdas consecutivas' }, - ]; - - // A7: mock sessions/logins for the security screen (mirrors /v1/security/*). - CT.securitySessions = [ - { id: 's1', created_at: '2026-07-18T08:00:00+00:00', last_seen_at: '2026-07-18T09:30:00+00:00', - ip: '187.20.14.2', user_agent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/126.0', remember: false, current: true }, - { id: 's2', created_at: '2026-07-16T21:10:00+00:00', last_seen_at: '2026-07-17T07:45:00+00:00', - ip: '177.94.3.71', user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5) Safari/604.1', remember: true, current: false }, - ]; - CT.securityLogins = [ - { id: 93, ts: '2026-07-18T08:00:00+00:00', action: 'login', actor: 'demo@criptotrade.dev', - ip: '187.20.14.2', ua: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/126.0', success: true }, - { id: 90, ts: '2026-07-17T23:41:00+00:00', action: 'login', actor: 'demo@criptotrade.dev', - ip: '45.12.9.30', ua: 'Mozilla/5.0 (Windows NT 10.0) Firefox/128.0', success: false }, - { id: 88, ts: '2026-07-16T21:10:00+00:00', action: 'login', actor: 'demo@criptotrade.dev', - ip: '177.94.3.71', ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5) Safari/604.1', success: true }, - ]; - - // A6: mock notification channels/rules (mirrors /v1/notifications, masked). - CT.notificationChannels = [ - { id: 'ch1', kind: 'telegram', label: 'Ops crítico', enabled: true, - config_masked: { bot_token: '•••4821', chat_id: '-100200300' }, - destination_masked: 'chat -100200300 · token •••4821', - last_test_at: '2026-07-18T09:00:00+00:00', last_test_ok: true, last_error: null }, - { id: 'ch2', kind: 'email', label: 'E-mail do dono', enabled: true, - config_masked: { to_email: 'dono@criptotrade.dev' }, - destination_masked: 'dono@criptotrade.dev', - last_test_at: null, last_test_ok: null, last_error: null }, - ]; - CT.notificationRules = [ - { id: 'r1', alert_type: 'circuit_breaker', min_severity: 'critical', - channel_ids: ['ch1', 'ch2'], pairs: ['*'], enabled: true }, - { id: 'r2', alert_type: '*', min_severity: 'high', - channel_ids: ['ch1'], pairs: ['BTC/USDT'], enabled: true }, - ]; - - // A5: mock exchange connections + platform keys (masked, like /v1/exchanges). - CT.connections = [ - { id: 'cx1', exchange_id: 'binance', label: 'Binance testnet', scope: 'trade', - testnet: true, is_active: true, api_key_masked: '•••b3f1', - created_at: '2026-07-15T10:00:00+00:00', last_test_at: '2026-07-18T08:30:00+00:00', - last_test_ok: true, last_test_detail: { read_ok: true, trade_detected: true }, - revoked: false }, - { id: 'cx2', exchange_id: 'binance', label: 'Binance leitura', scope: 'read', - testnet: false, is_active: false, api_key_masked: '•••9a2c', - created_at: '2026-07-10T09:00:00+00:00', last_test_at: '2026-07-17T22:00:00+00:00', - last_test_ok: false, - last_test_detail: { read_ok: false, error: 'Invalid API-key, IP, or permissions (chave •••9a2c)' }, - revoked: false }, - ]; - CT.platformKeys = [ - { id: 'pk1', label: 'grafana-readonly', key_prefix: 'ctk_a1b2c3d4', scope: 'visualizador', - created_at: '2026-07-12T14:00:00+00:00', last_used_at: '2026-07-18T09:45:00+00:00', - revoked: false }, - ]; - - // A10: mock onboarding status. Mixed states for the guide screenshots/e2e - // (2 auto-done, 1 skipped, 2 pending). Only used when MOCK_ONBOARDING is - // set — the default mock boot behaves as "completed" (no wizard). - CT.onboarding = { - steps: [ - { id: 'connect_exchange', status: 'done_auto', - detail: 'Binance testnet · binance · testnet · teste ok' }, - { id: 'risk_capital', status: 'done_auto', detail: 'config de risco/capital alterada' }, - { id: 'strategy_agents', status: 'skipped', detail: 'pulado' }, - { id: 'review', status: 'pending', detail: '' }, - { id: 'start_dryrun', status: 'pending', detail: '' }, - ], - completed: false, - dismissed: false, - completed_at: null, - summary: { - connection: { label: 'Binance testnet', exchange: 'binance', scope: 'trade', - testnet: true, tested_ok: true }, - routing: 'paper', dry_run: true, autonomy_level: 2, - risk: { max_position_size_pct: 5.0, max_daily_loss_pct: 5.0 }, - pairs: 'BTC/USDT,ETH/USDT', initial_capital: 10000, - }, - }; - - window.CT = CT; -})(); diff --git a/docs/design/pages/e2e/console.spec.js b/docs/design/pages/e2e/console.spec.js index 9ef46e9..d347933 100644 --- a/docs/design/pages/e2e/console.spec.js +++ b/docs/design/pages/e2e/console.spec.js @@ -1,6 +1,7 @@ // E2E smoke for the React console (P3-5b). Runs against the built dist/ with // mock data, so it exercises the real shell + hash routing without a backend. import { test, expect } from "@playwright/test"; +import { installMockApi } from "./fixtures/mockApi.js"; const NAV = [ // N2: "Mesa" (multi-asset hub) is the first Operação item and the landing @@ -13,8 +14,9 @@ const ADMIN_NAV = ["Conta & Perfil", "Usuários & Permissões", "Conexões & Cha "Trilha de Auditoria", "Notificações & Canais", "Segurança & Sessões"]; test.beforeEach(async ({ page }) => { - // Screens read window.USE_MOCK_DATA and render mock data instead of fetching. - await page.addInitScript(() => { window.USE_MOCK_DATA = true; }); + // O console chama o apiClient real; o fixture page.route serve os dados (admin → + // grupo Administração; PAIRS_RICH opera 5 pares → landing na Mesa). + await installMockApi(page, { authMode: "user", role: "admin" }); }); test("loads the app shell with the full navigation", async ({ page }) => { @@ -40,7 +42,8 @@ test("default landing is the Mesa when >1 pair is operated (N2)", async ({ page }); test("single operated pair keeps Visão Geral as the landing", async ({ page }) => { - await page.addInitScript(() => { window.MOCK_OPERATED = ["BTC/USDT"]; }); + // 1 operado → a Mesa não sequestra o boot (operados.length > 1 é falso). + await installMockApi(page, { authMode: "user", role: "admin", operados: ["BTC/USDT"] }); await page.goto("/"); await expect(page.locator(".nav-item.active")).toContainText("Visão Geral"); }); diff --git a/docs/design/pages/e2e/desk.spec.js b/docs/design/pages/e2e/desk.spec.js index 2817201..0ef0826 100644 --- a/docs/design/pages/e2e/desk.spec.js +++ b/docs/design/pages/e2e/desk.spec.js @@ -2,9 +2,12 @@ // mixed states (open position, active signal, awaiting), so we assert the grid, // the summary row, and the row → Mercado drill-down (sets the global pair). import { test, expect } from "@playwright/test"; +import { installMockApi } from "./fixtures/mockApi.js"; test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { window.USE_MOCK_DATA = true; }); + // A Mesa chama GET /v1/desk/summary; o fixture serve as 5 linhas ricas (estados + // mistos, BNB paused) que antes vinham do _mockDesk() removido. + await installMockApi(page, { authMode: "user", role: "admin" }); }); test("Mesa is the landing and shows every operated pair (aceite 1)", async ({ page }) => { @@ -95,9 +98,8 @@ test("11c — heatmap respeita o badge PAUSADO", async ({ page }) => { }); test("11c — hint aparece com muitos pares (pós-filtro) e some ao dispensar", async ({ page }) => { - await page.addInitScript(() => { - window.MOCK_OPERATED = Array.from({ length: 14 }, (_, i) => `PAIR${i}/USDT`); - }); + // 14 linhas no desk summary → o hint de heatmap dispara (visible.length > 10). + await installMockApi(page, { authMode: "user", role: "admin", desk: 14 }); await page.goto("/#desk"); const hint = page.getByText(/experimente o modo heatmap/); await expect(hint).toBeVisible(); diff --git a/docs/design/pages/e2e/fixtures/datasets.js b/docs/design/pages/e2e/fixtures/datasets.js index 4169915..a985f2a 100644 --- a/docs/design/pages/e2e/fixtures/datasets.js +++ b/docs/design/pages/e2e/fixtures/datasets.js @@ -89,6 +89,26 @@ export function deskSummary() { }; } +// Desk summary com N linhas geradas (desk.spec: hint de heatmap aparece com >10 pares). +export function deskSummaryN(n) { + const regimes = ["strong_uptrend", "strong_downtrend", "sideways", "chaotic", "unknown"]; + const labels = { strong_uptrend: "Alta forte", strong_downtrend: "Baixa forte", sideways: "Lateral", chaotic: "Caótico", unknown: "Desconhecido" }; + const iso = (minsAgo) => new Date(Date.now() - minsAgo * 60000).toISOString(); + const rows = Array.from({ length: n }, (_, i) => { + const regime = regimes[i % regimes.length]; + const action = ["buy", "sell", null][i % 3]; + return { + symbol: `PAIR${i}/USDT`, last: 100 + i, change_24h_pct: ((i % 7) - 3) * 1.1, + regime, regime_label: labels[regime], signal_action: action, + signal_confidence: action ? 0.6 + (i % 4) * 0.1 : null, + position_side: null, position_qty: null, position_entry: null, unrealized_pnl: null, + as_of: iso(1 + (i % 5)), last_cycle_at: iso(1 + (i % 5)), paused: false, + }; + }); + return { rows, slots_used: 0, slots_max: 3, capital_allocated: 0, capital_free: 10000, + signals_active: rows.filter((r) => (r.signal_confidence || 0) >= 0.6).length }; +} + // GET /v1/onboarding/status — completed so the first-run redirect stays quiet. // GET /v1/onboarding/status — baseline COMPLETED: o boot dos outros specs não é // sequestrado. O cenário pending (onboarding.spec) é servido stateful por-teste. diff --git a/docs/design/pages/e2e/fixtures/mockApi.js b/docs/design/pages/e2e/fixtures/mockApi.js index 0a70e1f..c8dcaec 100644 --- a/docs/design/pages/e2e/fixtures/mockApi.js +++ b/docs/design/pages/e2e/fixtures/mockApi.js @@ -21,7 +21,7 @@ import { NOTIF_CHANNELS, NOTIF_RULES, NOTIF_SETTINGS, CONNECTIONS, PLATFORM_KEYS, EGRESS_IP, createdPlatformKey, ACCOUNT_PROFILE, PREFERENCES, SECURITY_SESSIONS, SECURITY_LOGINS, auditList, auditEvent, - onboardingPending, + onboardingPending, deskSummaryN, } from "./datasets.js"; // Wrap a payload in the API envelope the client's req() unwraps ({ data, meta }). @@ -111,12 +111,18 @@ const SSE_PATHS = new Set(["/v1/alerts"]); * @returns {{ unstubbed: string[] }} handle exposing any unstubbed endpoints hit */ export async function installMockApi(page, scenario = {}) { - const { authMode = "user", role = "admin", routes = {}, onboarding = null } = scenario; + const { authMode = "user", role = "admin", routes = {}, onboarding = null, + operados = null, desk = null } = scenario; const table = { ...baseline({ authMode, role }), ...routes }; const unstubbed = []; + // Stateful desk summary: N linhas geradas (hint) ou as 5 ricas padrão. O PATCH de + // par (pausar/retomar) sincroniza a linha aqui, então o reload da Mesa reflete (N9). + const deskState = desk != null ? deskSummaryN(desk) : deskSummary(); // Stateful operated pairs (N8²/N9): POST/PATCH/DELETE mutate; GET /v1/pairs reflects. - // Seeded fresh per test from PAIRS_RICH (BNB starts paused). - const operated = PAIRS_RICH.operados.map((o) => ({ ...o })); + // Seeded fresh por teste de PAIRS_RICH (BNB paused), ou de `operados` (ex.: single-pair). + const operated = operados + ? operados.map((s) => ({ symbol: s, status: "operando", paused: false, last_cycle_at: null })) + : PAIRS_RICH.operados.map((o) => ({ ...o })); const decode = (seg) => decodeURIComponent(seg).replace("-", "/"); // Stateful onboarding (onboarding.spec T1/T2) — só quando o cenário pede 'pending'; // caso contrário o baseline completed responde /v1/onboarding/status. @@ -147,6 +153,11 @@ export async function installMockApi(page, scenario = {}) { } } + // --- stateful desk summary (reflete pausar/retomar da Mesa, N9) -------- + if (path === "/v1/desk/summary" && method === "GET") { + return fulfillJson(route, deskState); + } + // --- stateful operated pairs ------------------------------------------- if (path === "/v1/pairs" && method === "GET") { return fulfillJson(route, { operados: operated, observaveis: PAIRS_RICH.observaveis }); @@ -159,8 +170,12 @@ export async function installMockApi(page, scenario = {}) { } const opMatch = path.match(/^\/v1\/pairs\/operated\/(.+)$/); if (opMatch && method === "PATCH") { - const row = operated.find((o) => o.symbol === decode(opMatch[1])); - if (row) row.paused = !!(req.postDataJSON() || {}).paused; + const sym = decode(opMatch[1]); + const paused = !!(req.postDataJSON() || {}).paused; + const row = operated.find((o) => o.symbol === sym); + if (row) row.paused = paused; + const drow = deskState.rows.find((r) => r.symbol === sym); // N9: sincroniza a Mesa + if (drow) drow.paused = paused; return fulfillJson(route, row || {}); } if (opMatch && method === "DELETE") { diff --git a/docs/design/pages/e2e/onboarding.spec.js b/docs/design/pages/e2e/onboarding.spec.js index ffd8980..dc126f2 100644 --- a/docs/design/pages/e2e/onboarding.spec.js +++ b/docs/design/pages/e2e/onboarding.spec.js @@ -3,18 +3,18 @@ // render honestly, completing leads to the dashboard, and non-admins never // see it. Baseline fixture = completed, so every other spec boots unchanged. // -// Acoplamento parcial (5.6b): o REDIRECT no boot mora em app.jsx (core, 5.7) e é -// dirigido pela flag legada window.MOCK_ONBOARDING; o fixture serve o cenário -// pending (/v1/onboarding/status) + o PATCH stateful. Os dois convivem até a 5.7 -// remover a leitura da flag junto com o resto do core. Os testes de deep-link / -// menu / 403 já rodam em fixture puro. +// 100% fixture (5.7 removeu a flag legada MOCK_ONBOARDING do app.jsx): o redirect +// no boot é dirigido só por GET /v1/onboarding/status (cenário pending via +// installMockApi({onboarding:'pending'}) + o PATCH stateful). A corrida com a +// landing da Mesa resolve deterministicamente para o guia (o auto-open sobrescreve; +// a Mesa relê o hash na resolução e cede). import { test, expect } from "@playwright/test"; import { installMockApi } from "./fixtures/mockApi.js"; test("pending status opens the guide on boot (aceite 1)", async ({ page }) => { + // Sem flags: o redirect é 100% do caminho real — GET /v1/onboarding/status=pending + // → o auto-open navega p/ #onboarding; a Mesa cede (relê o hash na resolução). await installMockApi(page, { authMode: "user", role: "admin", onboarding: "pending" }); - // flag legada: dispara o redirect determinístico no app.jsx (sem corrida com a Mesa) - await page.addInitScript(() => { window.USE_MOCK_DATA = true; window.MOCK_ONBOARDING = "pending"; }); await page.goto("/"); await expect(page).toHaveURL(/#onboarding$/); await expect(page.locator(".page-title")).toContainText("Guia de configuração"); @@ -31,7 +31,6 @@ test("pending status opens the guide on boot (aceite 1)", async ({ page }) => { test("completing the remaining steps leads to the dashboard (aceite 2)", async ({ page }) => { await installMockApi(page, { authMode: "user", role: "admin", onboarding: "pending" }); - await page.addInitScript(() => { window.USE_MOCK_DATA = true; window.MOCK_ONBOARDING = "pending"; }); await page.goto("/"); await page.getByRole("button", { name: "Revisei — está tudo certo" }).click(); await page.getByRole("button", { name: "Pular", exact: true }).click(); // start_dryrun @@ -49,10 +48,9 @@ test("default fixture (completed) boots to the dashboard, not the guide", async }); test("an explicit deep link is never hijacked", async ({ page }) => { - // MOCK_ONBOARDING=pending prova que o deep-link resiste ao redirect; o Agentes - // de-mockado serve /v1/agents=[] pelo fixture → o

Agentes

aparece. - await installMockApi(page, { authMode: "user", role: "admin" }); - await page.addInitScript(() => { window.USE_MOCK_DATA = true; window.MOCK_ONBOARDING = "pending"; }); + // onboarding=pending prova que o deep-link resiste ao redirect (os dois effects do + // app.jsx dão early-return no hash não-vazio); o Agentes serve /v1/agents=[] → título. + await installMockApi(page, { authMode: "user", role: "admin", onboarding: "pending" }); await page.goto("/#agents"); await expect(page.locator(".page-title")).toContainText("Agentes"); }); diff --git a/docs/design/pages/e2e/pairs_path.spec.js b/docs/design/pages/e2e/pairs_path.spec.js index 284eba3..972c710 100644 --- a/docs/design/pages/e2e/pairs_path.spec.js +++ b/docs/design/pages/e2e/pairs_path.spec.js @@ -3,9 +3,12 @@ // (dois segmentos → 404). Este teste intercepta a requisição REAL do apiClient // (não depende de backend nem do mock das telas) e afirma a forma proxy-safe. import { test, expect } from "@playwright/test"; +import { installMockApi } from "./fixtures/mockApi.js"; test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { window.USE_MOCK_DATA = true; }); + // Boot via fixture; o route específico do teste (abaixo) é registrado depois e + // vence para /v1/pairs/operated/** (captura as URLs de PATCH/DELETE). + await installMockApi(page, { authMode: "user", role: "admin" }); }); test("apiClient: PATCH/DELETE de par usam hífen no path (não %2F)", async ({ page }) => { diff --git a/docs/design/pages/e2e/system.spec.js b/docs/design/pages/e2e/system.spec.js index f62b1f3..69b1953 100644 --- a/docs/design/pages/e2e/system.spec.js +++ b/docs/design/pages/e2e/system.spec.js @@ -5,7 +5,7 @@ import { installMockApi } from "./fixtures/mockApi.js"; test("unknown deep-link lands on the 404 page, not a blank overview", async ({ page }) => { const errors = []; page.on("pageerror", (e) => errors.push(e.message)); - await page.addInitScript(() => { window.USE_MOCK_DATA = true; }); + await installMockApi(page, { authMode: "user", role: "admin" }); await page.goto("/#does-not-exist"); await expect(page.getByRole("heading", { name: "Página não encontrada" })).toBeVisible(); await page.getByRole("button", { name: "Voltar ao início" }).click(); @@ -14,10 +14,7 @@ test("unknown deep-link lands on the 404 page, not a blank overview", async ({ p }); test("route without permission shows 403 with the required permission", async ({ page }) => { - await page.addInitScript(() => { - window.USE_MOCK_DATA = true; - window.MOCK_ROLE = "visualizador"; - }); + await installMockApi(page, { authMode: "user", role: "visualizador" }); await page.goto("/#users"); await expect(page.getByRole("heading", { name: "Sem permissão" })).toBeVisible(); await expect(page.getByText("manage_users")).toBeVisible(); @@ -25,24 +22,17 @@ test("route without permission shows 403 with the required permission", async ({ }); test("admin still reaches the users screen directly", async ({ page }) => { - // Users de-mockada (5.4) → serve /v1/users + /v1/roles pelo fixture; USE_MOCK_DATA - // segue só para a auth mock admin (o route-guard). Coexistência transitória. await installMockApi(page, { authMode: "user", role: "admin" }); - await page.addInitScript(() => { window.USE_MOCK_DATA = true; }); await page.goto("/#users"); await expect(page.locator(".page-title")).toContainText("Usuários & Permissões"); }); test("a screen exception hits the boundary with an error id and recovers", async ({ page }) => { - // A recuperação navega para o Mercado, já de-mockado (5.3b) → serve seus dados - // pelo fixture; USE_MOCK_DATA segue ligado só para o hook de exceção do Overview - // (que ainda usa mock, sai na 5.7). Coexistência transitória. - await installMockApi(page, { authMode: "user" }); - await page.addInitScript(() => { - window.USE_MOCK_DATA = true; - // Test hook: makes the Overview screen throw during render. - window.MOCK_THROW_SCREEN = "overview"; - }); + // Seam de teste: __E2E_THROW_SCREEN força uma exceção de RENDER no Overview + // (fault-injection síncrona — o error-boundary precisa de throw em render, não + // dá para simular por dados do fixture). Inerte na produção (nunca injetado). + await installMockApi(page, { authMode: "user", role: "admin" }); + await page.addInitScript(() => { window.__E2E_THROW_SCREEN = "overview"; }); await page.goto("/#overview"); await expect(page.getByText("Erro inesperado nesta tela")).toBeVisible(); await expect(page.getByText(/erro [a-z0-9]+-[a-z0-9]+/)).toBeVisible(); diff --git a/docs/design/pages/index.html b/docs/design/pages/index.html index 0835840..3d46787 100644 --- a/docs/design/pages/index.html +++ b/docs/design/pages/index.html @@ -20,7 +20,6 @@ so the console reaches the API same-origin over HTTPS at /api. --> - diff --git a/docs/design/pages/screen_desk.jsx b/docs/design/pages/screen_desk.jsx index a3959b1..d04368e 100644 --- a/docs/design/pages/screen_desk.jsx +++ b/docs/design/pages/screen_desk.jsx @@ -27,47 +27,6 @@ const _SIGNAL_CLS = { buy: 'badge-ok', sell: 'badge-down' }; const _PAUSED_TIP = 'Pausado — sem novas ordens; posições abertas seguem geridas (stop/TP ativos)'; -// Deterministic mock — 5 majors in MIXED states (position aberta, sinal ativo, -// aguardando), so the demo mirrors the real Mesa and screenshots stay stable. -// e2e: window.MOCK_OPERATED (array) força um conjunto arbitrário (ex.: 12+ pares -// para o hint) — determinístico por índice, sem backend. -function _mockDesk() { - const now = new Date(); - const iso = (minsAgo) => new Date(now - minsAgo * 60000).toISOString(); - const override = (typeof window !== 'undefined' && window.MOCK_OPERATED) || null; - if (override && override.length) { - const regimes = ['strong_uptrend', 'strong_downtrend', 'sideways', 'chaotic', 'unknown']; - const actions = ['buy', 'sell', null]; - const rows = override.map((symbol, i) => { - const regime = regimes[i % regimes.length]; - const action = actions[i % actions.length]; - return { - symbol, last: 100 + i, change_24h_pct: ((i % 7) - 3) * 1.1, - regime, regime_label: _REGIME_META[regime].label, - signal_action: action, - signal_confidence: action ? 0.6 + (i % 4) * 0.1 : null, - position_side: null, position_qty: null, position_entry: null, unrealized_pnl: null, - as_of: iso(1 + (i % 5)), last_cycle_at: iso(1 + (i % 5)), paused: false, - }; - }); - return { rows, slots_used: 0, slots_max: 3, capital_allocated: 0, capital_free: 10000, - signals_active: rows.filter(r => (r.signal_confidence || 0) >= 0.6).length }; - } - const rows = [ - { symbol: 'SOL/USDT', last: 160.42, change_24h_pct: 4.81, regime: 'strong_uptrend', regime_label: 'Alta forte', - signal_action: 'buy', signal_confidence: 0.90, position_side: 'buy', position_qty: 1.2, position_entry: 150.1, unrealized_pnl: 12.38, as_of: iso(1), last_cycle_at: iso(1) }, - { symbol: 'BTC/USDT', last: 64810.0, change_24h_pct: 2.34, regime: 'strong_uptrend', regime_label: 'Alta forte', - signal_action: 'buy', signal_confidence: 0.82, position_side: 'buy', position_qty: 0.03, position_entry: 61200.0, unrealized_pnl: 108.30, as_of: iso(1), last_cycle_at: iso(1) }, - { symbol: 'ETH/USDT', last: 3208.5, change_24h_pct: -1.12, regime: 'sideways', regime_label: 'Lateral', - signal_action: 'sell', signal_confidence: 0.71, position_side: null, position_qty: null, position_entry: null, unrealized_pnl: null, as_of: iso(2), last_cycle_at: iso(2) }, - { symbol: 'BNB/USDT', last: 592.3, change_24h_pct: 0.42, regime: 'chaotic', regime_label: 'Caótico', paused: true, - signal_action: 'hold', signal_confidence: 0.34, position_side: null, position_qty: null, position_entry: null, unrealized_pnl: null, as_of: iso(2), last_cycle_at: iso(2) }, - { symbol: 'XRP/USDT', last: 0.61, change_24h_pct: 1.05, regime: 'unknown', regime_label: 'Desconhecido', - signal_action: null, signal_confidence: null, position_side: null, position_qty: null, position_entry: null, unrealized_pnl: null, as_of: null, last_cycle_at: null }, - ]; - return { rows, slots_used: 2, slots_max: 3, capital_allocated: 3673.0, capital_free: 6327.0, signals_active: 3 }; -} - function _rel(iso) { if (!iso) return '—'; const secs = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000)); @@ -85,7 +44,6 @@ function _sortRows(rows, by) { } function ScreenDesk({ navigate, addToast } = {}) { - const mock = window.USE_MOCK_DATA; const [data, setData] = useState(null); const [error, setError] = useState(null); const [sortBy, setSortBy] = useState('default'); @@ -96,17 +54,11 @@ function ScreenDesk({ navigate, addToast } = {}) { const canEdit = CT_AUTH.can('edit_settings'); const load = () => { - if (mock) { setData(_mockDesk()); return; } CT_API.getDeskSummary().then((d) => { setData(d); setError(null); }).catch(setError); }; // N9: pausar/retoma um par direto da Mesa — aplica por ciclo, sem restart. const pausePair = async (sym, paused) => { - if (mock) { - setData((d) => ({ ...d, rows: d.rows.map((r) => r.symbol === sym ? { ...r, paused } : r) })); - addToast?.(paused ? 'Par pausado.' : 'Par retomado.', 'check'); - return; - } try { await CT_API.setPairPaused(sym, paused); addToast?.(paused ? 'Par pausado — sem novas ordens; posições seguem geridas.' : 'Par retomado.', 'check'); @@ -115,7 +67,6 @@ function ScreenDesk({ navigate, addToast } = {}) { }; useEffect(() => { load(); }, []); useEffect(() => { - if (mock) return; const id = setInterval(load, 15000); // server TTL is ~8s; poll a bit slower return () => clearInterval(id); }, []); diff --git a/docs/design/pages/screen_market.jsx b/docs/design/pages/screen_market.jsx index 8dc665d..78d78ea 100644 --- a/docs/design/pages/screen_market.jsx +++ b/docs/design/pages/screen_market.jsx @@ -44,7 +44,7 @@ function SRLevelRow({ label, price, strength, color }) { ); } -// Static regime legend — the set of regimes is a fixed enum (was CT.regime.options mock). +// Static regime legend — the set of regimes is a fixed enum (antes vinha do mock global). const REGIME_OPTIONS = [ { key: 'strong_uptrend', label: 'Alta forte', desc: 'Tendência de alta consistente', strat: 'Trend-following' }, { key: 'strong_downtrend', label: 'Baixa forte', desc: 'Tendência de baixa consistente', strat: 'Trend-following (venda)' }, diff --git a/docs/design/pages/screen_overview.jsx b/docs/design/pages/screen_overview.jsx index ec786da..7c853c3 100644 --- a/docs/design/pages/screen_overview.jsx +++ b/docs/design/pages/screen_overview.jsx @@ -19,21 +19,21 @@ const O_STATUS = { }; function ScreenOverview() { - // A9 e2e hook: lets the boundary test force a render exception (mock only). - if (window.USE_MOCK_DATA && window.MOCK_THROW_SCREEN === 'overview') { + // A9 seam de teste e2e: o teste do error-boundary força uma exceção de render + // injetando window.__E2E_THROW_SCREEN via addInitScript. Inerte na produção + // (nunca injetado no bundle servido) — é fault-injection, não dado mockado. + if (window.__E2E_THROW_SCREEN === 'overview') { throw new Error('Falha simulada para teste do boundary'); } - const mock = !!window.USE_MOCK_DATA; const [scope] = useCurrentPair(); const [period, setPeriod] = useState('7d'); const [metrics, setMetrics] = useState(null); const [equity, setEquity] = useState(null); const [orders, setOrders] = useState(null); - const [loading, setLoading] = useState(!mock); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = () => { - if (mock) return; setLoading(true); setError(null); const sym = scope; @@ -53,16 +53,14 @@ function ScreenOverview() { .catch(e => { setError(e); setLoading(false); }); }; - useEffect(() => { load(); }, [scope, period, mock]); + useEffect(() => { load(); }, [scope, period]); // Honest formatting: null ≠ 0 → "Sem dados". const ratio = (v) => (v == null ? 'Sem dados' : fmtNum(v)); const pct = (v) => (v == null ? 'Sem dados' : `${fmtNum(+v * 100, 1)}%`); let body; - if (mock) { - body = ; - } else if (loading) { + if (loading) { body = ; } else if (error) { body = ; diff --git a/docs/design/pages/screen_risk.jsx b/docs/design/pages/screen_risk.jsx index 9682145..6a35db9 100644 --- a/docs/design/pages/screen_risk.jsx +++ b/docs/design/pages/screen_risk.jsx @@ -73,14 +73,14 @@ function ScreenRisk() { if (loading) return ; if (error) return { setError(null); setLoading(true); }} />; - // Capital KPIs from the real portfolio metrics (was the CT.capital mock). + // Capital KPIs from the real portfolio metrics (antes vinham do mock global). const cap = metrics ? { value: metrics.portfolio_value_usdt, pnlPct: (metrics.pnl_period_pct ?? 0) * 100, exposurePct: (metrics.exposure_pct ?? 0) * 100, openPositions: metrics.open_positions, } : null; - // Header badge derived from the real protections (was CT.drawdown.overallStatus). + // Header badge derived from the real protections (antes derivado do mock global). const overallStatus = (protections || []).some(p => p.status && p.status !== 'ok') ? 'warn' : 'ok'; const cbArmed = cb?.status === 'armed'; const kellyOk = kelly?.data_quality === 'ok'; diff --git a/docs/design/pages/shell.jsx b/docs/design/pages/shell.jsx index 83e2827..0fe36c0 100644 --- a/docs/design/pages/shell.jsx +++ b/docs/design/pages/shell.jsx @@ -86,7 +86,6 @@ window.Sidebar = Sidebar; // fmtPrice now lives in components.jsx (window.fmtPrice) — single source of truth (M7). function Header({ onToggleAlerts, alertCount, auth, onLock, onLogout, onNavigate }) { - const mock = !!window.USE_MOCK_DATA; const [health, setHealth] = useState(null); const [hitl, setHitl] = useState(null); const [pair, setPair] = useState(CT_PAIR.get()); @@ -106,16 +105,16 @@ function Header({ onToggleAlerts, alertCount, auth, onLock, onLogout, onNavigate const isAll = pair === 'ALL'; useEffect(() => { - if (mock || isAll) { setTicker(null); return; } + if (isAll) { setTicker(null); return; } let alive = true; CT_API.getTicker(pair) .then(t => { if (alive) setTicker(t); }) .catch(() => { if (alive) setTicker(null); }); return () => { alive = false; }; - }, [pair, mock, isAll]); + }, [pair, isAll]); - const price = ticker?.last ?? CT.symbol?.price ?? 65200; - const change = ticker?.change_24h_pct ?? CT.symbol?.change24h ?? 0; + const price = ticker?.last ?? 65200; + const change = ticker?.change_24h_pct ?? 0; return (