Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions docs/design/pages/e2e/fixtures/datasets.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,57 @@ export const MARKET_SIGNAL = {
valid_until: new Date(Date.now() + 3600e3).toISOString(), as_of: new Date().toISOString(),
};

// GET /v1/hitl/config — the header's autonomy badge.
export const HITL_CONFIG = { level: 1, threshold_usdt: 100.0, label: "Assistido" };
// GET /v1/hitl/config — header autonomy badge + the HITL Controls screen.
export const HITL_CONFIG = {
level: 1, current_level: 1, threshold_usdt: 100.0, label: "Assistido",
level_description: "Ordens acima do limite exigem aprovação humana.",
pending_orders_count: 2, human_approved_today: 5, human_rejected_today: 1,
levels: [
{ level: 0, threshold_usdt: 0, description: "Manual — toda ordem exige aprovação." },
{ level: 1, threshold_usdt: 100, description: "Assistido — ordens acima do limite exigem aprovação." },
{ level: 2, threshold_usdt: 500, description: "Semi-autônomo — só ordens grandes exigem aprovação." },
{ level: 3, threshold_usdt: 999999, description: "Autônomo — sem aprovação (confirmação extra)." },
],
};

// GET /v1/orders — pending queue (HITL) + Orders table. ≥2 pares incl. BTC/USDT
// para os chips de par (N4); confidence (Fix #3) + confidence_breakdown por ordem.
export const ORDERS = [
{ id: "ord_a1", pair: "BTC/USDT", side: "buy", quantity: 0.03, price: 64500, entry: 64500,
stop_loss: 63200, take_profit: 66800, position_size_pct: 2, confidence: 0.82, status: "pending",
strategy: "trend_following", agent_id: "strategy_agent", reason: "Rompimento confirmado por volume.",
created_at: new Date().toISOString(),
confidence_breakdown: [{ key: "trend", score: 0.85 }, { key: "momentum", score: 0.7 }, { key: "volume", score: 0.8 }] },
{ id: "ord_b2", pair: "ETH/USDT", side: "buy", quantity: 0.5, price: 3200, entry: 3200,
stop_loss: 3080, take_profit: 3450, position_size_pct: 2, confidence: 0.71, status: "pending",
strategy: "mean_reversion", agent_id: "strategy_agent", reason: "Reversão no suporte confirmada.",
created_at: new Date().toISOString(),
confidence_breakdown: [{ key: "trend", score: 0.6 }, { key: "momentum", score: 0.75 }, { key: "volume", score: 0.8 }] },
];

// GET /v1/trades — trades fechados (P&L realizado por par, N5). ≥2 símbolos incl. BTC/USDT.
export const TRADES = [
{ order_id: "t1", symbol: "BTC/USDT", side: "buy", entry_price: 61200, exit_price: 64810, quantity: 0.03, fee: 0.5, pnl: 108.30, pnl_pct: 5.9 },
{ order_id: "t2", symbol: "BTC/USDT", side: "buy", entry_price: 65000, exit_price: 63600, quantity: 0.03, fee: 0.5, pnl: -42.10, pnl_pct: -2.1 },
{ order_id: "t3", symbol: "ETH/USDT", side: "buy", entry_price: 3100, exit_price: 3223, quantity: 0.5, fee: 0.3, pnl: 61.50, pnl_pct: 4.0 },
{ order_id: "t4", symbol: "SOL/USDT", side: "buy", entry_price: 150, exit_price: 160.3, quantity: 1.2, fee: 0.2, pnl: 12.38, pnl_pct: 1.5 },
{ order_id: "t5", symbol: "SOL/USDT", side: "buy", entry_price: 148, exit_price: 168.7, quantity: 1.2, fee: 0.2, pnl: 24.90, pnl_pct: 3.0 },
{ order_id: "t6", symbol: "XRP/USDT", side: "buy", entry_price: 0.62, exit_price: 0.60, quantity: 400, fee: 0.1, pnl: -8.20, pnl_pct: -1.3 },
];

// GET /v1/users — exatamente 3 (rbac test 3 afirma tbody tr === 3).
export const USERS = [
{ id: "u_demo", name: "Operador Demo", email: "demo@criptotrade.dev", role: "admin", status: "active", last_login_at: new Date().toISOString(), invite_id: null },
{ id: "u_ana", name: "Ana", email: "ana@criptotrade.dev", role: "operador", status: "active", last_login_at: new Date(Date.now() - 86400e3).toISOString(), invite_id: null },
{ id: "u_novo", name: null, email: "novo@criptotrade.dev", role: "visualizador", status: "pending", last_login_at: null, invite_id: "inv_1" },
];

// GET /v1/roles — a "Matriz de permissões" renderiza de r.permissions.
export const ROLES = [
{ id: "admin", label: "Admin", permissions: ["approve_order", "change_autonomy", "change_risk", "edit_settings", "manage_keys", "view_audit", "manage_users"] },
{ id: "operador", label: "Operador", permissions: ["approve_order", "change_autonomy", "view_audit"] },
{ id: "visualizador", label: "Visualizador", permissions: [] },
];

// GET /v1/market/{pair}/ticker — the header price (when a single pair is selected).
export function ticker(pair) {
Expand Down
10 changes: 7 additions & 3 deletions docs/design/pages/e2e/fixtures/mockApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
AGENTS, authMe, candles, deskSummary, EQUITY, HEALTH, HITL_CONFIG, MARKET_INDICATORS,
JOURNAL, JOURNAL_METRICS, MARKET_LEVELS, MARKET_PATTERNS, MARKET_REGIME, MARKET_SIGNAL,
MARKET_VOLUME_PROFILE,
metrics, ONBOARDING, PAIRS_FLAT, PAIRS_RICH, processEvents, RISK_CIRCUIT_BREAKER,
RISK_CONFIG, RISK_KELLY, RISK_PROTECTIONS, RISK_SKIPS, RISK_SLOTS, SYS_CONFIG, ticker,
metrics, ONBOARDING, ORDERS, PAIRS_FLAT, PAIRS_RICH, processEvents, RISK_CIRCUIT_BREAKER,
RISK_CONFIG, RISK_KELLY, RISK_PROTECTIONS, RISK_SKIPS, RISK_SLOTS, ROLES, SYS_CONFIG,
ticker, TRADES, USERS,
} from "./datasets.js";

// Wrap a payload in the API envelope the client's req() unwraps ({ data, meta }).
Expand All @@ -41,7 +42,10 @@ function baseline({ authMode, role }) {
"GET /v1/metrics/equity": () => EQUITY,
"GET /v1/onboarding/status": () => ONBOARDING,
"GET /v1/desk/summary": () => deskSummary(),
"GET /v1/orders": () => [], // sidebar pending-count poll → empty
"GET /v1/orders": () => ORDERS, // sidebar poll + HITL queue + Ordens
"GET /v1/trades": () => TRADES, // Ordens: P&L realizado por par (N5)
"GET /v1/users": () => USERS, // Usuários & Permissões (exatamente 3)
"GET /v1/roles": () => ROLES, // matriz de permissões
// Risco (screen_risk)
"GET /v1/risk/protections": () => RISK_PROTECTIONS,
"GET /v1/risk/circuit-breaker": () => RISK_CIRCUIT_BREAKER,
Expand Down
3 changes: 2 additions & 1 deletion docs/design/pages/e2e/pair_dimension.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// N4: HITL ganha mini-contexto do par (preço/regime) + filtro por par na fila.
// N5: Ordens ganham o rodapé de P&L realizado por par no modo ∑.
import { test, expect } from "@playwright/test";
import { installMockApi } from "./fixtures/mockApi.js";

test.beforeEach(async ({ page }) => {
await page.addInitScript(() => { window.USE_MOCK_DATA = true; });
await installMockApi(page, { authMode: "user" });
});

test("N4 — HITL: cada ordem mostra o mini-contexto do par (preço atual)", async ({ page }) => {
Expand Down
19 changes: 6 additions & 13 deletions docs/design/pages/e2e/rbac.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// A3 RBAC in the console: role-driven visibility (visualizador hides actions,
// demo shows them disabled with the discovery tooltip, admin sees everything).
// Cenários de auth/role vêm do fixture (/v1/auth/me), não mais de MOCK_ROLE/MOCK_AUTH.
import { test, expect } from "@playwright/test";
import { installMockApi } from "./fixtures/mockApi.js";

test("visualizador sees no approve/reject and no admin nav", async ({ page }) => {
await page.addInitScript(() => {
window.USE_MOCK_DATA = true;
window.MOCK_ROLE = "visualizador";
});
await installMockApi(page, { authMode: "user", role: "visualizador" });
await page.goto("/#hitl");
await expect(page.locator(".page-title")).toContainText("HITL");
await expect(page.getByRole("button", { name: "Aprovar" })).toHaveCount(0);
Expand All @@ -18,10 +17,7 @@ test("visualizador sees no approve/reject and no admin nav", async ({ page }) =>
});

test("demo mode shows actions disabled with the discovery tooltip", async ({ page }) => {
await page.addInitScript(() => {
window.USE_MOCK_DATA = true;
window.MOCK_AUTH = "demo";
});
await installMockApi(page, { authMode: "demo" });
await page.goto("/#hitl");
const approve = page.getByRole("button", { name: "Aprovar" }).first();
await expect(approve).toBeVisible();
Expand All @@ -34,7 +30,7 @@ test("demo mode shows actions disabled with the discovery tooltip", async ({ pag
});

test("admin sees the Administração group and the users screen", async ({ page }) => {
await page.addInitScript(() => { window.USE_MOCK_DATA = true; });
await installMockApi(page, { authMode: "user", role: "admin" });
await page.goto("/");
await page.locator(".nav-item", { hasText: "Usuários & Permissões" }).click();
await expect(page).toHaveURL(/#users$/);
Expand All @@ -44,10 +40,7 @@ test("admin sees the Administração group and the users screen", async ({ page
});

test("operador keeps approve buttons but has no admin nav", async ({ page }) => {
await page.addInitScript(() => {
window.USE_MOCK_DATA = true;
window.MOCK_ROLE = "operador";
});
await installMockApi(page, { authMode: "user", role: "operador" });
await page.goto("/#hitl");
await expect(page.getByRole("button", { name: "Aprovar" }).first()).toBeEnabled();
await expect(page.locator(".nav-item", { hasText: "Usuários & Permissões" }))
Expand Down
3 changes: 3 additions & 0 deletions docs/design/pages/e2e/system.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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");
Expand Down
56 changes: 7 additions & 49 deletions docs/design/pages/screen_hitl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function PendingOrderCard({ order, onDecide, highlight = false, ctx = null }) {
</p>
)}

<ConfidenceBreakdown breakdown={CT.confidenceBreakdown} />
<ConfidenceBreakdown breakdown={order.confidence_breakdown} />

{!canApprove && !demoView && (
<div style={{ marginTop: 14, display: 'flex', alignItems: 'center', gap: 7,
Expand Down Expand Up @@ -161,31 +161,9 @@ function PendingOrderCard({ order, onDecide, highlight = false, ctx = null }) {
}

function ScreenHITL({ addToast }) {
const mock = !!window.USE_MOCK_DATA;

const mockConfig = {
current_level: CT.hitl.level,
threshold_usdt: CT.hitl.threshold,
level_description: CT.hitl.levels[CT.hitl.level]?.desc ?? '',
pending_orders_count: CT.pendingOrders.length,
human_approved_today: CT.hitl.approvedToday,
human_rejected_today: CT.hitl.rejectedToday,
levels: CT.hitl.levels.map(l => ({
level: l.level,
threshold_usdt: l.threshold,
description: l.desc,
})),
};
const mockOrders = CT.pendingOrders.map(o => ({
...o,
stop_loss: o.stop,
take_profit: o.takeProfit,
position_size_pct: o.sizePct,
}));

const [config, setConfig] = useState(mock ? mockConfig : null);
const [orders, setOrders] = useState(mock ? mockOrders : []);
const [loading, setLoading] = useState(!mock);
const [config, setConfig] = useState(null);
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// M2: pair handed over by Mercado's "Ver no HITL" — highlight its orders once.
const [focusPair] = useState(() => {
Expand All @@ -199,24 +177,16 @@ function ScreenHITL({ addToast }) {
const [pairFilter, setPairFilter] = useState(focusPair);
// N4: per-pair context (preço atual + regime) from the desk snapshot — fetched
// ONCE here and distributed to the cards, never one request per order.
const [deskCtx, setDeskCtx] = useState(() => mock ? {
'BTC/USDT': { last: 64810, regime_label: 'Alta forte' },
'ETH/USDT': { last: 3208, regime_label: 'Lateral' },
'SOL/USDT': { last: 160.42, regime_label: 'Alta forte' },
'XRP/USDT': { last: 0.61, regime_label: 'Desconhecido' },
'BNB/USDT': { last: 592, regime_label: 'Caótico' },
} : {});
const [deskCtx, setDeskCtx] = useState(() => ({}));
useEffect(() => {
if (mock) return;
CT_API.getDeskSummary().then(d => {
const map = {};
(d.rows || []).forEach(r => { map[r.symbol] = { last: r.last, regime_label: r.regime_label }; });
setDeskCtx(map);
}).catch(() => {});
}, [mock]);
}, []);

const load = useCallback(() => {
if (mock) return;
setLoading(true);
Promise.all([
CT_API.getHITL(),
Expand All @@ -228,15 +198,11 @@ function ScreenHITL({ addToast }) {
setLoading(false);
})
.catch(e => { setError(e); setLoading(false); });
}, [mock]);
}, []);

useEffect(() => { load(); }, [load]);

const decide = async (orderId, action, note) => {
if (mock) {
setOrders(prev => prev.filter(o => o.id !== orderId));
return;
}
try {
// A3: no client-sent operator — the server stamps the session identity.
await CT_API.decideOrder(orderId, {
Expand All @@ -252,14 +218,6 @@ function ScreenHITL({ addToast }) {
};

const setLevel = async (level) => {
if (mock) {
setConfig(prev => ({
...prev,
current_level: level,
level_description: prev.levels[level]?.description ?? '',
}));
return;
}
try {
// A3: no client-sent operator — the server stamps the session identity.
const updated = await CT_API.patchHITL({
Expand Down
25 changes: 4 additions & 21 deletions docs/design/pages/screen_orders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,15 @@ const ORDER_STATUS_LABEL = {
};

function ScreenOrders() {
const mock = !!window.USE_MOCK_DATA;
const mockOrders = CT.orders.map(o => ({
...o,
stop_loss: o.stop,
take_profit: o.takeProfit,
position_size_pct: o.sizePct,
operator_note: o.operatorNote,
}));

// N5: mock closed trades (realized P&L) for the per-pair footer.
const mockTrades = [
{ symbol: 'BTC/USDT', pnl: 108.30 }, { symbol: 'BTC/USDT', pnl: -42.10 },
{ symbol: 'ETH/USDT', pnl: 61.50 }, { symbol: 'SOL/USDT', pnl: 12.38 },
{ symbol: 'SOL/USDT', pnl: 24.90 }, { symbol: 'XRP/USDT', pnl: -8.20 },
];

const [scope] = useCurrentPair();
const [orders, setOrders] = useState(mock ? mockOrders : null);
const [trades, setTrades] = useState(mock ? mockTrades : null);
const [loading, setLoading] = useState(!mock);
const [orders, setOrders] = useState(null);
const [trades, setTrades] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [statusF, setStatusF] = useState('all');
const [sideF, setSideF] = useState('all');

useEffect(() => {
if (mock) return;
setLoading(true);
const pairQ = scope && scope !== 'ALL' ? `&pair=${encodeURIComponent(scope)}` : '';
Promise.all([CT_API.getOrders(100, 0, pairQ), CT_API.getTrades(200, 0, pairQ).catch(() => [])])
Expand All @@ -55,7 +38,7 @@ function ScreenOrders() {
setLoading(false);
})
.catch(e => { setError(e); setLoading(false); });
}, [mock, scope]);
}, [scope]);

if (loading) return <LoadingState label="Carregando ordens…" />;
if (error) return <ErrorState message="Erro ao carregar ordens" onRetry={() => { setError(null); setLoading(true); }} />;
Expand Down
11 changes: 4 additions & 7 deletions docs/design/pages/screen_users.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,22 @@ function InviteModal({ roles, onClose, onInvited, addToast }) {
}

function ScreenUsers({ addToast }) {
const mock = !!window.USE_MOCK_DATA;
const [users, setUsers] = useUsersState(mock ? (CT.adminUsers ?? []) : null);
const [roles, setRoles] = useUsersState(mock ? (CT.roles ?? []) : []);
const [loading, setLoading] = useUsersState(!mock);
const [users, setUsers] = useUsersState(null);
const [roles, setRoles] = useUsersState([]);
const [loading, setLoading] = useUsersState(true);
const [error, setError] = useUsersState(null);
const [inviting, setInviting] = useUsersState(false);

const load = useUsersCallback(() => {
if (mock) return;
setLoading(true);
Promise.all([CT_API.getUsers(), CT_API.getRoles()])
.then(([u, r]) => { setUsers(u); setRoles(r); setLoading(false); setError(null); })
.catch(e => { setError(e); setLoading(false); });
}, [mock]);
}, []);

useUsersEffect(() => { load(); }, [load]);

const act = async (fn, okMsg) => {
if (mock) { addToast?.('Modo demo: ação não aplicada.', 'info'); return; }
try { await fn(); addToast?.(okMsg, 'check'); load(); }
catch (e) { addToast?.(e?.message ?? 'Falha na ação.', 'alert'); }
};
Expand Down
Loading