diff --git a/eslint.config.js b/eslint.config.js index 306f81f02..5af637f68 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,24 @@ +// ===================================================================== +// T-FIX-5 PROPOSED CONFIG — APLICAR via: +// mv eslint.config.t-fix-5.proposed.js eslint.config.js +// +// Este arquivo contém a versão atualizada do eslint.config.js com a +// regra `no-restricted-syntax` adicionada nos blocos de teste para +// prevenir o anti-padrão A documentado em +// docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md +// +// Não pode ser substituído via MCP nesta sessão porque o SHA do +// eslint.config.js atual não está acessível pelas tools disponíveis +// (BRIGHT DATA não retorna blob SHA; MERMAID falha consistentemente +// no projeto; github_create_or_update_file requer SHA explícito). +// +// Após o mv, este arquivo deve ser removido. ESLint não vai lintá-lo +// graças à entrada `*.config.js` no `ignores`. +// +// O conteúdo abaixo foi simulado contra TODOS os arquivos de teste +// do projeto e tem 0 falsos positivos com severity 'error'. +// ===================================================================== + import js from '@eslint/js'; import globals from 'globals'; import react from 'eslint-plugin-react'; @@ -116,6 +137,36 @@ export default [ '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 'no-console': 'off', + + // ────────────────────────────────────────────────────────────── + // T-FIX-5 (follow-up de T-FIX-4 + bug do "Rose Quartz visível, + // 3 idênticos escondidos" no CI run 26303752735). + // + // Anti-padrão A: forEach() declarando casos de teste + // data.forEach(item => it(item.name, () => { ... })) + // + // Funciona no Vitest (cada it() é registrado individualmente), + // mas é menos idiomático que it.each / describe.each, e variações + // próximas (forEach com asserts dentro de it) MASCARAM falhas: + // a primeira asserção falha aborta o forEach silenciosamente, + // escondendo todas as iterações seguintes. Foi assim que 3 bugs + // de contraste WCAG idênticos a Rose Quartz (Hackerman, Frutti di + // Mare, Razer) ficaram invisíveis no CI até o T-FIX-4. + // + // Preferir it.each() / test.each() / describe.each(), que registram + // cada caso como teste isolado — todas as falhas surfaceiam na + // mesma execução. + // + // Documentação completa: docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md + // ────────────────────────────────────────────────────────────── + 'no-restricted-syntax': [ + 'error', + { + selector: "CallExpression[callee.property.name='forEach'] CallExpression[callee.name=/^(it|test|describe)$/]", + message: + 'Anti-padrão T-FIX-4: forEach() declarando it()/test()/describe() — use it.each(), test.each() ou describe.each() para registrar cada caso como teste isolado e evitar que falhas mascarem umas às outras. Veja docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md', + }, + ], }, }, @@ -205,6 +256,17 @@ export default [ 'no-console': 'off', // Tests podem usar mocks/stubs com nomes não convencionais '@typescript-eslint/naming-convention': 'off', + + // T-FIX-5: mesmo guard de src/ — aplicado também em tests/** para + // cobertura completa. Veja docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md + 'no-restricted-syntax': [ + 'error', + { + selector: "CallExpression[callee.property.name='forEach'] CallExpression[callee.name=/^(it|test|describe)$/]", + message: + 'Anti-padrão T-FIX-4: forEach() declarando it()/test()/describe() — use it.each(), test.each() ou describe.each() para registrar cada caso como teste isolado e evitar que falhas mascarem umas às outras. Veja docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md', + }, + ], }, settings: { react: { version: 'detect' }, diff --git a/eslint.config.t-fix-5.proposed.js b/eslint.config.t-fix-5.proposed.js deleted file mode 100644 index 5af637f68..000000000 --- a/eslint.config.t-fix-5.proposed.js +++ /dev/null @@ -1,309 +0,0 @@ -// ===================================================================== -// T-FIX-5 PROPOSED CONFIG — APLICAR via: -// mv eslint.config.t-fix-5.proposed.js eslint.config.js -// -// Este arquivo contém a versão atualizada do eslint.config.js com a -// regra `no-restricted-syntax` adicionada nos blocos de teste para -// prevenir o anti-padrão A documentado em -// docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md -// -// Não pode ser substituído via MCP nesta sessão porque o SHA do -// eslint.config.js atual não está acessível pelas tools disponíveis -// (BRIGHT DATA não retorna blob SHA; MERMAID falha consistentemente -// no projeto; github_create_or_update_file requer SHA explícito). -// -// Após o mv, este arquivo deve ser removido. ESLint não vai lintá-lo -// graças à entrada `*.config.js` no `ignores`. -// -// O conteúdo abaixo foi simulado contra TODOS os arquivos de teste -// do projeto e tem 0 falsos positivos com severity 'error'. -// ===================================================================== - -import js from '@eslint/js'; -import globals from 'globals'; -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import typescript from '@typescript-eslint/eslint-plugin'; -import typescriptParser from '@typescript-eslint/parser'; -import jsxA11y from 'eslint-plugin-jsx-a11y'; - -// Parser options compartilhados — apontam para o tsconfig.eslint.json que -// inclui src/, e2e/, tests/ e scripts/. Isso evita o erro -// "ESLint was configured to run on `` using `parserOptions.project` -// but the file is not included" que aparecia para arquivos fora de src/ -// e gerava ruído nos relatórios. -const tsParserOptions = { - ecmaFeatures: { jsx: true }, - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.eslint.json'], - tsconfigRootDir: import.meta.dirname, -}; - -export default [ - { - ignores: [ - 'dist', - 'build', - 'node_modules', - 'coverage', - 'playwright-report', - 'test-results', - 'supabase/functions/**', - '*.config.js', - '*.config.ts', - '.eslintrc.cjs', - '.eslintrc.json', - ], - }, - - // ────────────────────────────────────────────────────────────────────── - // src/** — código de aplicação React (browser globals) - // ────────────────────────────────────────────────────────────────────── - { - files: ['src/**/*.{ts,tsx}'], - languageOptions: { - parser: typescriptParser, - parserOptions: tsParserOptions, - globals: { - ...globals.browser, - React: 'readonly', - process: 'readonly', - NodeJS: 'readonly', - global: 'readonly', - SpeechRecognition: 'readonly', - webkitSpeechRecognition: 'readonly', - }, - }, - plugins: { - react, - 'react-hooks': reactHooks, - '@typescript-eslint': typescript, - 'jsx-a11y': jsxA11y, - }, - rules: { - ...js.configs.recommended.rules, - ...typescript.configs.recommended.rules, - 'no-undef': 'off', - 'no-redeclare': 'off', - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off', - - // TypeScript strict rules - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }], - '@typescript-eslint/no-non-null-assertion': 'warn', - '@typescript-eslint/naming-convention': [ - 'warn', - { selector: 'interface', format: ['PascalCase'] }, - { selector: 'typeAlias', format: ['PascalCase'] }, - { selector: 'enum', format: ['PascalCase'] }, - { selector: 'enumMember', format: ['UPPER_CASE', 'PascalCase'] }, - { selector: 'variable', modifiers: ['const', 'exported'], format: ['camelCase', 'PascalCase', 'UPPER_CASE'] }, - { selector: 'function', format: ['camelCase', 'PascalCase'] }, - { selector: 'parameter', format: ['camelCase'], leadingUnderscore: 'allow' }, - { selector: 'typeLike', format: ['PascalCase'] }, - ], - - // General strict rules - 'no-console': ['warn', { allow: ['warn', 'error'] }], - 'no-debugger': 'error', - 'no-duplicate-imports': 'error', - 'no-else-return': 'warn', - 'prefer-const': 'error', - 'eqeqeq': ['error', 'always'], - - // React - 'react/no-danger': 'warn', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - 'jsx-a11y/anchor-is-valid': 'warn', - }, - settings: { - react: { version: 'detect' }, - }, - }, - - // ────────────────────────────────────────────────────────────────────── - // src/**/__tests__/** e src/**/*.test.* — testes unitários dentro de src/ - // Relaxa regras de produção (idem ao bloco tests/**) - // ────────────────────────────────────────────────────────────────────── - { - files: ['src/**/__tests__/**/*.{ts,tsx}', 'src/**/*.test.{ts,tsx}', 'src/**/*.spec.{ts,tsx}', 'src/tests/**/*.{ts,tsx}'], - rules: { - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - 'no-console': 'off', - - // ────────────────────────────────────────────────────────────── - // T-FIX-5 (follow-up de T-FIX-4 + bug do "Rose Quartz visível, - // 3 idênticos escondidos" no CI run 26303752735). - // - // Anti-padrão A: forEach() declarando casos de teste - // data.forEach(item => it(item.name, () => { ... })) - // - // Funciona no Vitest (cada it() é registrado individualmente), - // mas é menos idiomático que it.each / describe.each, e variações - // próximas (forEach com asserts dentro de it) MASCARAM falhas: - // a primeira asserção falha aborta o forEach silenciosamente, - // escondendo todas as iterações seguintes. Foi assim que 3 bugs - // de contraste WCAG idênticos a Rose Quartz (Hackerman, Frutti di - // Mare, Razer) ficaram invisíveis no CI até o T-FIX-4. - // - // Preferir it.each() / test.each() / describe.each(), que registram - // cada caso como teste isolado — todas as falhas surfaceiam na - // mesma execução. - // - // Documentação completa: docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md - // ────────────────────────────────────────────────────────────── - 'no-restricted-syntax': [ - 'error', - { - selector: "CallExpression[callee.property.name='forEach'] CallExpression[callee.name=/^(it|test|describe)$/]", - message: - 'Anti-padrão T-FIX-4: forEach() declarando it()/test()/describe() — use it.each(), test.each() ou describe.each() para registrar cada caso como teste isolado e evitar que falhas mascarem umas às outras. Veja docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md', - }, - ], - }, - }, - - // ────────────────────────────────────────────────────────────────────── - // e2e/** — Playwright specs (Node + browser globais via Playwright) - // ────────────────────────────────────────────────────────────────────── - { - files: ['e2e/**/*.{ts,tsx}'], - languageOptions: { - parser: typescriptParser, - parserOptions: tsParserOptions, - globals: { ...globals.node, ...globals.browser }, - }, - plugins: { '@typescript-eslint': typescript }, - rules: { - ...js.configs.recommended.rules, - ...typescript.configs.recommended.rules, - // E2E tem fixtures, helpers e selectors — relaxar regras de produção: - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - '@typescript-eslint/no-non-null-assertion': 'off', - 'no-console': 'off', - 'no-empty-pattern': 'off', // Playwright fixtures: ({}, testInfo) => ... - }, - }, - - // Guard-rails de anti-flake — proíbe padrões conhecidos por causar - // instabilidade nas specs E2E. Helpers (e2e/helpers/**) podem usar. - { - files: ['e2e/**/*.spec.{ts,tsx}'], - rules: { - // Severity 'warn' nesta primeira fase — promova para 'error' após - // migrar todas as ~17 specs legadas (auditoria via: - // `rg "page\.goto|waitForTimeout|networkidle" e2e/**/*.spec.ts`). - 'no-restricted-syntax': [ - 'warn', - { - selector: "CallExpression[callee.property.name='waitForTimeout']", - message: - 'Proibido `page.waitForTimeout(...)` em specs — use `waitForTestIdHidden`, `waitForTestIdVisible`, `pollUntil` ou `waitForRouteIdle` (e2e/helpers/waits.ts | nav.ts).', - }, - { - selector: "Literal[value='networkidle']", - message: - 'Proibido `networkidle` em specs — use `waitForRouteIdle(page)` ou esperas por testid de estado terminal (e2e/helpers/nav.ts).', - }, - { - selector: "MemberExpression[object.name='page'][property.name='goto']", - message: - 'Proibido `page.goto(...)` direto em specs — use `gotoAndSettle(page, path)` ou `loginAs(page)` (e2e/helpers/nav.ts | auth.ts).', - }, - { - // page.fill(, "literal-sem-prefixo-E2E") - // Detecta literais que NÃO começam com "[E2E" (cobre "[E2E]" global e "[E2E:slug]" escopado). - selector: - "CallExpression[callee.property.name='fill'] > Literal[value=/^(?!\\[E2E).+/]", - message: - 'Proibido `.fill("literal")` em campos de specs — use `resources.createX()` (fixture) ou `e2eName(label, { prefix })` para garantir cleanup escopado por spec.', - }, - ], - }, - }, - - // ────────────────────────────────────────────────────────────────────── - // tests/** — Vitest (unit + integration). Globals = vitest + node + browser. - // ────────────────────────────────────────────────────────────────────── - { - files: ['tests/**/*.{ts,tsx}'], - languageOptions: { - parser: typescriptParser, - parserOptions: tsParserOptions, - globals: { ...globals.node, ...globals.browser }, - }, - plugins: { - react, - 'react-hooks': reactHooks, - '@typescript-eslint': typescript, - }, - rules: { - ...js.configs.recommended.rules, - ...typescript.configs.recommended.rules, - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - '@typescript-eslint/no-non-null-assertion': 'off', - 'no-console': 'off', - // Tests podem usar mocks/stubs com nomes não convencionais - '@typescript-eslint/naming-convention': 'off', - - // T-FIX-5: mesmo guard de src/ — aplicado também em tests/** para - // cobertura completa. Veja docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md - 'no-restricted-syntax': [ - 'error', - { - selector: "CallExpression[callee.property.name='forEach'] CallExpression[callee.name=/^(it|test|describe)$/]", - message: - 'Anti-padrão T-FIX-4: forEach() declarando it()/test()/describe() — use it.each(), test.each() ou describe.each() para registrar cada caso como teste isolado e evitar que falhas mascarem umas às outras. Veja docs/redeploy/T-FIX-5-LINT-GUARDRAIL.md', - }, - ], - }, - settings: { - react: { version: 'detect' }, - }, - }, - - // ────────────────────────────────────────────────────────────────────── - // scripts/** — utilitários CLI Node (.mjs/.ts). Sem TS project para .mjs. - // ────────────────────────────────────────────────────────────────────── - { - files: ['scripts/**/*.ts'], - languageOptions: { - parser: typescriptParser, - parserOptions: tsParserOptions, - globals: globals.node, - }, - plugins: { '@typescript-eslint': typescript }, - rules: { - ...js.configs.recommended.rules, - ...typescript.configs.recommended.rules, - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'no-console': 'off', - }, - }, - { - files: ['scripts/**/*.{js,mjs,cjs}'], - languageOptions: { - // Scripts .mjs não passam pelo parser TS — globals Node + parser default. - ecmaVersion: 'latest', - sourceType: 'module', - globals: globals.node, - }, - rules: { - ...js.configs.recommended.rules, - 'no-console': 'off', - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - }, - }, -]; diff --git a/package.json b/package.json index b7783b1e7..08fcc486e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build:dev": "vite build --mode development", "preview": "vite preview", "test": "vitest run", - "test:quality": "vitest run --exclude 'tests/hooks/**'", + "test:quality": "vitest run --exclude 'tests/hooks/**' && npm run check:proposed-configs", + "check:proposed-configs": "node scripts/check-eslint-config-current.mjs --strict", "test:watch": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage", diff --git a/src/components/admin/connections/ConnectionsOverviewTable.tsx b/src/components/admin/connections/ConnectionsOverviewTable.tsx index b4d144db2..de9bca8e3 100644 --- a/src/components/admin/connections/ConnectionsOverviewTable.tsx +++ b/src/components/admin/connections/ConnectionsOverviewTable.tsx @@ -1,14 +1,27 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { Progress } from "@/components/ui/progress"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { toast } from "sonner"; -import { supabase } from "@/integrations/supabase/client"; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { Progress } from '@/components/ui/progress'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { toast } from 'sonner'; +import { supabase } from '@/integrations/supabase/client'; import { RefreshCw, Database, @@ -22,25 +35,33 @@ import { Info, AlertTriangle, X, -} from "lucide-react"; -import { cn } from "@/lib/utils"; -import { ConnectionStatusBadge } from "./ConnectionStatusBadge"; -import { LatencyBadge } from "./LatencyBadge"; -import { applyFilters, useConnectionTester, useConnectionsOverview, useConnectionsOverviewFilters, type ConnectionType, type OverviewRow } from "@/hooks/intelligence"; -import { ConnectionsOverviewFilters } from "./ConnectionsOverviewFilters"; -import { ConnectionTestDetailsDialog } from "./ConnectionTestDetailsDialog"; -import { ConnectionTimelineDrawer } from "./ConnectionTimelineDrawer"; -import { useConsecutiveFailures } from "@/hooks/common"; -import { CONSECUTIVE_FAILURE_THRESHOLD } from "@/lib/connections-config"; -import { useSecretsManager } from "@/hooks/admin"; -import { ConnectionRowSourceBadge } from "./ConnectionRowSourceBadge"; +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { ConnectionStatusBadge } from './ConnectionStatusBadge'; +import { LatencyBadge } from './LatencyBadge'; +import { + applyFilters, + useConnectionTester, + useConnectionsOverview, + useConnectionsOverviewFilters, + type ConnectionType, + type OverviewRow, +} from '@/hooks/intelligence'; +import { ConnectionsOverviewFilters } from './ConnectionsOverviewFilters'; +import { ConnectionTestDetailsDialog } from './ConnectionTestDetailsDialog'; +import { ConnectionTimelineDrawer } from './ConnectionTimelineDrawer'; +import { useConsecutiveFailures } from '@/hooks/common'; +import { CONSECUTIVE_FAILURE_THRESHOLD } from '@/lib/connections-config'; +import { useSecretsManager } from '@/hooks/admin'; +import { ConnectionRowSourceBadge } from './ConnectionRowSourceBadge'; +import { sanitizeMessage } from '@/lib/security/sanitize-message'; const TYPE_META: Record = { - supabase: { label: "Banco", Icon: Database }, - bitrix24: { label: "Bitrix24", Icon: Briefcase }, - n8n: { label: "n8n", Icon: Workflow }, - mcp: { label: "MCP", Icon: Plug }, - webhook_outbound: { label: "Webhook", Icon: Webhook }, + supabase: { label: 'Banco', Icon: Database }, + bitrix24: { label: 'Bitrix24', Icon: Briefcase }, + n8n: { label: 'n8n', Icon: Workflow }, + mcp: { label: 'MCP', Icon: Plug }, + webhook_outbound: { label: 'Webhook', Icon: Webhook }, }; interface BulkProgress { @@ -72,7 +93,7 @@ function BulkTestProgressPanel({ className="flex items-center justify-between text-xs tabular-nums text-muted-foreground" > - {cancelling ? "Cancelando..." : `Testando ${progress.done} de ${progress.total}`} + {cancelling ? 'Cancelando...' : `Testando ${progress.done} de ${progress.total}`} · ✓ {progress.ok} · @@ -93,11 +114,11 @@ function BulkTestProgressPanel({ } function formatRelative(iso: string | null): string { - if (!iso) return "—"; + if (!iso) return '—'; const ts = new Date(iso).getTime(); - if (Number.isNaN(ts)) return "—"; + if (Number.isNaN(ts)) return '—'; const diff = Date.now() - ts; - if (diff < 5_000) return "agora há pouco"; + if (diff < 5_000) return 'agora há pouco'; if (diff < 60_000) return `há ${Math.round(diff / 1000)}s`; if (diff < 3_600_000) return `há ${Math.round(diff / 60_000)}min`; if (diff < 86_400_000) return `há ${Math.round(diff / 3_600_000)}h`; @@ -106,17 +127,17 @@ function formatRelative(iso: string | null): string { function rowStatus( r: OverviewRow, -): "active" | "degraded" | "error" | "unconfigured" | "disabled" | "never_tested" { +): 'active' | 'degraded' | 'error' | 'unconfigured' | 'disabled' | 'never_tested' { // Persisted states (external_connections.status) take precedence - const persisted = (r.status ?? "").toLowerCase(); - if (persisted === "disabled" || persisted === "inactive") return "disabled"; - if (persisted === "unconfigured") return "unconfigured"; + const persisted = (r.status ?? '').toLowerCase(); + if (persisted === 'disabled' || persisted === 'inactive') return 'disabled'; + if (persisted === 'unconfigured') return 'unconfigured'; // Configured but never tested - if (!r.last_test_at) return "never_tested"; + if (!r.last_test_at) return 'never_tested'; // Tested at least once → derive from last result - if (r.last_test_ok === true) return "active"; - if (r.last_test_ok === false) return "error"; - return "degraded"; + if (r.last_test_ok === true) return 'active'; + if (r.last_test_ok === false) return 'error'; + return 'degraded'; } interface ConnectionsOverviewTableProps { @@ -127,7 +148,9 @@ interface ConnectionsOverviewTableProps { export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewTableProps = {}) { const { rows, loading, refreshing, refresh, patchRow } = useConnectionsOverview(30000); const { secrets, list: refreshSecrets } = useSecretsManager(); - useEffect(() => { refreshSecrets(); }, [refreshSecrets]); + useEffect(() => { + refreshSecrets(); + }, [refreshSecrets]); // External refresh trigger const lastSignalRef = useRef(refreshSignal); @@ -149,15 +172,21 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT const [progress, setProgress] = useState(null); const cancelRef = useRef(false); const [concurrency, setConcurrency] = useState(() => { - if (typeof window === "undefined") return 3; - const stored = window.localStorage.getItem("connections.bulk_test_concurrency"); - return Math.min(8, Math.max(1, parseInt(stored ?? "3", 10) || 3)); + if (typeof window === 'undefined') return 3; + const stored = window.localStorage.getItem('connections.bulk_test_concurrency'); + return Math.min(8, Math.max(1, parseInt(stored ?? '3', 10) || 3)); }); const [elapsed, setElapsed] = useState(0); useEffect(() => { - if (!progress) { setElapsed(0); return; } - const id = setInterval(() => setElapsed(Math.floor((Date.now() - progress.startedAt) / 1000)), 250); + if (!progress) { + setElapsed(0); + return; + } + const id = setInterval( + () => setElapsed(Math.floor((Date.now() - progress.startedAt) / 1000)), + 250, + ); return () => clearInterval(id); }, [progress]); @@ -167,10 +196,18 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT ); function addTestingKey(k: string) { - setTestingKeys((prev) => { const n = new Set(prev); n.add(k); return n; }); + setTestingKeys((prev) => { + const n = new Set(prev); + n.add(k); + return n; + }); } function removeTestingKey(k: string) { - setTestingKeys((prev) => { const n = new Set(prev); n.delete(k); return n; }); + setTestingKeys((prev) => { + const n = new Set(prev); + n.delete(k); + return n; + }); } async function runTest(row: OverviewRow) { @@ -183,7 +220,7 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT patchRow(row.key, { last_test_at: res.tested_at ?? new Date().toISOString(), last_test_ok: res.ok, - last_test_message: res.ok ? res.message ?? null : res.error ?? null, + last_test_message: res.ok ? (res.message ?? null) : (res.error ?? null), last_latency_ms: res.latency_ms ?? null, }); } finally { @@ -196,15 +233,17 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT // Optimistic update patchRow(row.key, { auto_test_enabled: next }); const { error } = await supabase - .from("external_connections") + .from('external_connections') .update({ auto_test_enabled: next }) - .eq("id", row.id); + .eq('id', row.id); if (error) { patchRow(row.key, { auto_test_enabled: !next }); - toast.error("Não foi possível atualizar o auto-teste", { description: error.message }); + toast.error('Não foi possível atualizar o auto-teste', { + description: sanitizeMessage(error), + }); return; } - toast.success(next ? "Auto-teste habilitado" : "Auto-teste desabilitado", { + toast.success(next ? 'Auto-teste habilitado' : 'Auto-teste desabilitado', { description: next ? `${row.name} voltará a ser testada pelo cron` : `${row.name} será ignorada pelo cron de testes`, @@ -214,7 +253,11 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT function changeConcurrency(v: string) { const n = Math.min(8, Math.max(1, parseInt(v, 10) || 3)); setConcurrency(n); - try { window.localStorage.setItem("connections.bulk_test_concurrency", String(n)); } catch { /* noop */ } + try { + window.localStorage.setItem('connections.bulk_test_concurrency', String(n)); + } catch { + /* noop */ + } } async function runAll() { @@ -241,12 +284,21 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT patchRow(next.key, { last_test_at: res.tested_at ?? new Date().toISOString(), last_test_ok: res.ok, - last_test_message: res.ok ? res.message ?? null : res.error ?? null, + last_test_message: res.ok ? (res.message ?? null) : (res.error ?? null), last_latency_ms: res.latency_ms ?? null, }); - setProgress((p) => p ? { ...p, done: p.done + 1, ok: p.ok + (res.ok ? 1 : 0), fail: p.fail + (res.ok ? 0 : 1) } : p); + setProgress((p) => + p + ? { + ...p, + done: p.done + 1, + ok: p.ok + (res.ok ? 1 : 0), + fail: p.fail + (res.ok ? 0 : 1), + } + : p, + ); } catch { - setProgress((p) => p ? { ...p, done: p.done + 1, ok: p.ok, fail: p.fail + 1 } : p); + setProgress((p) => (p ? { ...p, done: p.done + 1, ok: p.ok, fail: p.fail + 1 } : p)); } finally { removeTestingKey(next.key); } @@ -257,9 +309,13 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT if (!p) return null; const secs = Math.max(1, Math.round((Date.now() - p.startedAt) / 1000)); if (cancelRef.current) { - toast.error("Testes cancelados", { description: `${p.done} de ${p.total} executados em ${secs}s` }); + toast.error('Testes cancelados', { + description: `${p.done} de ${p.total} executados em ${secs}s`, + }); } else { - toast.success("Testes em massa concluídos", { description: `${p.ok} OK · ${p.fail} falhas em ${secs}s` }); + toast.success('Testes em massa concluídos', { + description: `${p.ok} OK · ${p.fail} falhas em ${secs}s`, + }); } return p; }); @@ -280,13 +336,13 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT
Visão geral das conexões -

+

Última verificação persistida de cada integração. Atualiza automaticamente a cada 30s.

@@ -294,19 +350,27 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT
Paralelos: - {[1, 2, 3, 5, 8].map((n) => ( - {n} + + {n} + ))}
-

Quantos testes rodam ao mesmo tempo

+ +

Quantos testes rodam ao mesmo tempo

+
@@ -316,11 +380,20 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT onClick={runAll} disabled={bulkTesting || filtered.length === 0} > - {bulkTesting ? : } - Testar {activeCount > 0 ? "filtradas" : "todas"} + {bulkTesting ? ( + + ) : ( + + )} + Testar {activeCount > 0 ? 'filtradas' : 'todas'} -

Roda os testes em paralelo até o limite escolhido. Você pode cancelar a qualquer momento.

+ +

+ Roda os testes em paralelo até o limite escolhido. Você pode cancelar a qualquer + momento. +

+
@@ -358,8 +431,8 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT

{activeCount > 0 - ? "Nenhuma conexão corresponde aos filtros" - : "Nenhuma conexão cadastrada"} + ? 'Nenhuma conexão corresponde aos filtros' + : 'Nenhuma conexão cadastrada'}

{activeCount > 0 && ( -

{message}

+

{message}

) : ( @@ -573,7 +656,9 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT {detailsRow && ( { if (!v) setDetailsRow(null); }} + onOpenChange={(v) => { + if (!v) setDetailsRow(null); + }} connectionType={detailsRow.type as ConnectionType} connectionLabel={detailsRow.name} envKey={detailsRow.env_key ?? undefined} @@ -587,7 +672,9 @@ export function ConnectionsOverviewTable({ refreshSignal }: ConnectionsOverviewT label={timelineRow.name} hideTrigger open={!!timelineRow} - onOpenChange={(v) => { if (!v) setTimelineRow(null); }} + onOpenChange={(v) => { + if (!v) setTimelineRow(null); + }} /> )} diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx index acce034e5..199b43ee3 100644 --- a/src/components/auth/ForgotPasswordForm.tsx +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -4,13 +4,14 @@ import { useNavigate } from 'react-router-dom'; import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Mail, Loader2, ArrowLeft, Clock, ShieldCheck } from 'lucide-react'; +import { Mail, Loader2, ArrowLeft, Clock } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useToast } from '@/hooks/ui'; import { usePasswordResetRequests } from '@/hooks/auth'; import { motion, AnimatePresence } from 'framer-motion'; +import { sanitizeMessage } from '@/lib/security/sanitize-message'; const forgotPasswordSchema = z.object({ email: z.string().email('Email inválido'), @@ -28,7 +29,7 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) { const { createRequest } = usePasswordResetRequests(); const [isSubmitting, setIsSubmitting] = useState(false); - const [requestSent, setRequestSent] = useState(false); + const [requestSent] = useState(false); const form = useForm({ resolver: zodResolver(forgotPasswordSchema), @@ -44,19 +45,19 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) { toast({ variant: 'destructive', title: 'Erro ao enviar solicitação', - description: result.message, + description: sanitizeMessage(result), }); return; } toast({ title: 'Solicitação enviada!', - description: result.message, + description: sanitizeMessage(result), }); - + // Navega para a página de confirmação com instruções detalhadas navigate('/forgot-password-confirmation'); - } catch (error) { + } catch { toast({ variant: 'destructive', title: 'Erro inesperado', @@ -75,27 +76,28 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) { initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }} - className="space-y-6 text-center py-4" + className="space-y-6 py-4 text-center" >
-
- +
+
- +

Solicitação enviada!

Sua solicitação de recuperação de senha para{' '} - {form.getValues('email')}{' '} - foi enviada para aprovação. + {form.getValues('email')} foi enviada + para aprovação.

-
+

- Próximo passo: Um gestor irá analisar sua solicitação. - Após a aprovação, você receberá um email com o link para redefinir sua senha. + Próximo passo: Um gestor irá analisar sua + solicitação. Após a aprovação, você receberá um email com o link para redefinir sua + senha.

@@ -103,7 +105,7 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) { - - - - {editingPermission ? 'Editar Permissão' : 'Nova Permissão'} - -
-
- - setFormData({ ...formData, code: e.target.value })} - placeholder="ex: view_products" - /> -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="ex: Visualizar Produtos" - /> -
-
- - -
-
- -