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
39 changes: 8 additions & 31 deletions docs/design/pages/apiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -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;
Expand Down
22 changes: 9 additions & 13 deletions docs/design/pages/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -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]);

Expand All @@ -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;
Expand Down
31 changes: 30 additions & 1 deletion docs/design/pages/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<Badge/>).
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)),
Expand Down
27 changes: 6 additions & 21 deletions docs/design/pages/components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,38 +372,23 @@ 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;
}
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;
}
Expand Down
Loading
Loading