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 (