fix(supabase): realign frontend with live DB schema#329
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
WalkthroughNormalizações defensivas e migração de queries para o cliente supabase, grande refatoração de useExpertChat (streaming + TTS), várias melhorias de UI/admin, tipagens de kill-switch/telemetria e uma migration SQL para restaurar trigger de quote_number. ChangesPrincipal
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Pull request overview
Este PR realinha o frontend com o schema real do Supabase (projeto doufsxqlfjyuvxuezpln), regenerando os tipos e corrigindo divergências que estavam mascaradas por um types.ts desatualizado — incluindo a restauração do trigger que gera quotes.quote_number, que estava quebrando a criação de orçamentos.
Changes:
- Restaura o trigger
set_quote_numberempublic.quotese ajusta inserts para permitir geração automática dequote_number. - Migra vários pontos de
untypedFrom()/casts parasupabase.from()tipado, além de corrigir drift de nulabilidade/mismatches de colunas. - Corrige arquivos que estavam inadvertidamente commitados em base64 (wrapper untyped e lint script) e normaliza formatação/typing em hooks e componentes.
Reviewed changes
Copilot reviewed 40 out of 41 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/20260525020848_restore_set_quote_number_trigger.sql | Restaura trigger BEFORE INSERT para gerar quote_number automaticamente. |
| src/pages/kit-builder/useKitBuilderQuote.ts | Passa quote_number: '' e ajusta payload para compatibilidade com trigger/schema. |
| src/pages/admin/SellerDiscountLimitsAdminPage.tsx | Filtra user_id nullable e normaliza upsert tipado. |
| src/lib/supabase-untyped.ts | Restaura wrapper untypedFrom() (antes base64) e documenta estratégia/guardrail. |
| src/lib/external-db/kill-switch-telemetry.ts | Remove cast any, usa supabase.from() tipado e ajusta lint. |
| src/lib/external-db/kill-switch-client.ts | Remove cast any, usa tabela tipada e melhora logs. |
| src/lib/external-db/bridge-status-events.ts | Ajusta tipos para Omit distribuído e formatação/robustez. |
| src/lib/external-db/tests/kill-switch-client.test.ts | Ajuste no mock para aceitar args e manter compatibilidade. |
| src/hooks/useKillSwitchBanner.ts | Ajuste de import para type e formatação. |
| src/hooks/simulation/useSimulation.ts | Ajuste de cast via unknown para compatibilidade com tipos gerados. |
| src/hooks/quotes/useQuoteVersions.ts | Normaliza queries/formatting e fallback para campos agora nullable nos tipos. |
| src/hooks/quotes/useQuoteTemplates.ts | Filtra user_id nullable ao montar lista de vendedores. |
| src/hooks/quotes/useDiscountApproval.ts | Normaliza queries tipadas, logs RLS e enriquecimento com fallbacks de nulabilidade. |
| src/hooks/quotes/quoteHelpers.ts | Ajusta payloads (quote_number/client_name/subtotal de itens) para refletir NOT NULL/required no DB. |
| src/hooks/products/useProductInsights.ts | Fallbacks para campos nullable e normalização de lógica/formatting. |
| src/hooks/products/useProductAnalytics.ts | Migra de untypedFrom() para supabase.from() e corrige insert de search_analytics para user_id. |
| src/hooks/kit-builder/useKitCollaboration.ts | Valida profile.user_id nullable antes de inserir colaborador. |
| src/hooks/intelligence/useSalesHistoryMacro.ts | Normaliza queries e trata seller_id nullable nos resultados. |
| src/hooks/intelligence/useSalesHistory.ts | Normaliza queries e trata seller_id/status nullable nos resultados. |
| src/hooks/intelligence/useSalesGoals.ts | Migra de untypedFrom() para supabase.from() tipado. |
| src/components/security/SecurityDashboard.tsx | Filtra user_id nullable antes de setar usuários. |
| src/components/search/useGlobalSearch.ts | Fallback para kit.name nullable ao montar resultado. |
| src/components/intelligence/TrendsHeatmap.tsx | Migra de untypedFrom() para supabase.from() tipado e ajusta UI. |
| src/components/intelligence/TopCategoriesCard.tsx | Migra de untypedFrom() para supabase.from() tipado e ajusta UI/formatting. |
| src/components/filters/FilterPresets.ts | Trata campos nullable (category/is_default/created_at/updated_at) e corrige deps do callback. |
| src/components/favorites/FavoritesTrashView.tsx | Aceita expires_at nullable e melhora layout/formatting. |
| src/components/expert/chat/useExpertChat.ts | Ajusta inserts de quotes para quote_number trigger + client_name NOT NULL e normaliza código. |
| src/components/dashboard/RecentKitsWidget.tsx | Trata nulabilidade em custom_kits (name/status/updated_at) e ajusta UI. |
| src/components/dashboard/MyRecentQuotesWidget.tsx | Trata updated_at nullable ao renderizar “tempo atrás”. |
| src/components/dashboard/MyClientsWidget.tsx | Normaliza updated_at nullable antes de agregações. |
| src/components/admin/users/useUserManagement.ts | Filtra user_id nullable e normaliza queries/formatting. |
| src/components/admin/users/RoleAuditLogPanel.tsx | Normaliza queries, typing e renderização do audit log. |
| src/components/admin/SellerDiscountLimitsPanel.tsx | Ajusta consulta/listagem; ainda há casts remanescentes a remover (comentado). |
| src/components/admin/security/role-migration/RoleMigrationPanel.tsx | Filtra user_id nullable e normaliza UI/formatting. |
| src/components/admin/security/RecentAuditTable.tsx | Normaliza UI/formatting e trata user_id nullable ao enriquecer profiles. |
| src/components/admin/security/keys/useMcpKeys.ts | Normaliza tipos/filters e trata user_id nullable em profiles lookup. |
| src/components/admin/security/keys/audit/useStepUpAttempts.ts | Normaliza queries/filters e trata user_id nullable ao enriquecer profiles. |
| src/components/admin/security/keys/audit/useMcpAuditFeed.ts | Trata user_id nullable no enriquecimento e normaliza parsing. |
| src/components/admin/security/ActiveIpsList.tsx | Ajusta typing: created_by pode ser null. |
| scripts/lint-untyped-from.sh | Restaura script (antes base64) e garante lint de untypedFrom() vs schema gerado. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| client_id: quote.client_id || null, | ||
| client_name: quote.client_name || null, | ||
| client_name: quote.client_name || '', | ||
| client_email: quote.client_email || null, | ||
| client_phone: quote.client_phone || null, | ||
| client_company: quote.client_company || null, |
| const { data, error: fetchError } = await supabase | ||
| .from('profiles') | ||
| .select('user_id, full_name') | ||
| .order('full_name'); | ||
|
|
| const { data: limits } = await supabase | ||
| .from("seller_discount_limits" as never) | ||
| .select("user_id, max_discount_percent") | ||
| .in("user_id", ids); | ||
| .from('seller_discount_limits' as never) | ||
| .select('user_id, max_discount_percent') | ||
| .in('user_id', ids); | ||
|
|
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const { error } = await (supabase as any) | ||
| .from("seller_discount_limits") | ||
| .upsert({ user_id: userId, max_discount_percent: percent, set_by: u.user.id }, { onConflict: "user_id" }); | ||
| .from('seller_discount_limits') | ||
| .upsert( |
| // Using type assertion since table was just created | ||
| // Silently insert - all errors are ignored for analytics to not affect UX |
| <Package className="h-4 w-4 text-primary" /> | ||
| </div> | ||
| <div className="min-w-0 flex-1"> | ||
| <p className="truncate text-sm font-medium">{kit.name}</p> |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ec719159ed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| subtotal: v.subtotal ?? 0, | ||
| discount_amount: v.discount_amount ?? 0, | ||
| discount_percent: v.discount_percent ?? 0, | ||
| created_at: v.created_at ?? '', |
There was a problem hiding this comment.
Preserve valid datetime when normalizing quote versions
When created_at is null, this normalization now writes an empty string, but downstream UIs (QuoteVersionHistory/QuoteVersionCompare) call format(new Date(version.created_at), ...). new Date('') is invalid and causes date-fns to throw at render time, so opening version history can break for any quote row with nullable timestamps (the regenerated Supabase type marks quotes.created_at as nullable). Use a valid fallback (or keep null and guard at render) instead of ''.
Useful? React with 👍 / 👎.
| .in('status', ['sent', 'approved', 'rejected', 'expired', 'converted']); | ||
| for (const q of quotes || []) { | ||
| quotesMap[q.id] = { seller_id: q.seller_id, status: q.status }; | ||
| quotesMap[q.id] = { seller_id: q.seller_id ?? '', status: q.status ?? '' }; |
There was a problem hiding this comment.
Avoid coercing nullable seller IDs to empty UUID strings
Coercing seller_id to '' introduces invalid UUID values into allSellerIds, which is later passed to .in('user_id', allSellerIds) on profiles. On projects where profiles.user_id is UUID (typical here), an empty string can trigger 22P02 invalid input syntax for type uuid, breaking seller-name enrichment for this query path whenever quotes/orders with null seller IDs exist. Keep nulls and filter falsy IDs before building the .in(...) filter.
Useful? React with 👍 / 👎.
|
|
||
| const quotesMap: Record<string, { seller_id: string }> = {}; | ||
| for (const q of quotesRes.data || []) quotesMap[q.id] = { seller_id: q.seller_id }; | ||
| for (const q of quotesRes.data || []) quotesMap[q.id] = { seller_id: q.seller_id ?? '' }; |
There was a problem hiding this comment.
Filter out null seller IDs before profiles lookup
This code also normalizes nullable seller_id to '' and then sends those values into .in('user_id', allSellerIds). If profiles.user_id is UUID, including '' can raise Postgres 22P02 invalid input syntax for type uuid, causing the macro sales history query to fail whenever a source row has null seller info. Preserve nulls and drop falsy IDs before issuing the profiles query.
Useful? React with 👍 / 👎.
Regenerate src/integrations/supabase/types.ts from the live database (141->296 tables, 1->115 views, 65->385 functions) so the typed client matches reality, then fix every drift the accurate types surfaced. - Decode src/lib/supabase-untyped.ts and scripts/lint-untyped-from.sh, which were committed base64-encoded since #319 and broke compilation. - Fix nullability drift across admin/security, dashboard, quotes and intelligence hooks (DB columns are nullable; code assumed non-null). - Fix real runtime bugs hidden by the stale types: - quotes.client_name is NOT NULL -> stop inserting null. - quote_items.subtotal is required -> compute it on insert. - search_analytics uses user_id (not seller_id) and has no filters_used column -> correct the insert. - Restore the set_quote_number BEFORE INSERT trigger (migration) lost in the migration-replay drift; without it quote creation fails the quote_number NOT NULL constraint. Inserts now pass an empty quote_number for the trigger to populate. - Migrate intelligence untypedFrom() calls to typed supabase.from(); drop now-unnecessary `as any` casts in the kill-switch client/telemetry. tsc baseline: 484 errors (-24 vs 508, no regressions). eslint baseline clean. Build and affected unit tests pass.
ec71915 to
18dc9e7
Compare
…ntInput main and this PR independently converged on a fix for the non-distributive Omit over the BridgeStatusEvent union. main shipped first (PR #328 chain) with a mapped-type `BridgeStatusEventInput`; this PR had an equivalent `DistributiveOmit`. Adopting main's version keeps a single source of truth and unblocks the merge.
…thouse placeholder + dynamic import)
…ut gradual + typed cast)
…ped cast for kill_switch_hits)
Status do merge (assistente)5 dos 6 conflitos resolvidos (commits
Verificado localmente que, após esses 5 commits, o único arquivo que ainda conflita é Último passo (1 comando local)git fetch origin
git checkout claude/frontend-supabase-bugs-SpsuE
git pull
git merge -X ours --no-edit origin/main
git push origin claude/frontend-supabase-bugs-SpsuEIsso cria o merge commit que mantém a regen completa (34k linhas) e ignora as ~353 adições de main (que já estão incluídas na regen, pois ela é snapshot do banco vivo). Depois o PR fica Por que não consegui fechar daquiSem token git no shell deste runtime — todas as escritas foram via API GitHub um arquivo por vez. O Validações já feitas
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 901525e67d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| import { useEffect, useState } from 'react'; | ||
| import { getKillSwitchState, KillSwitchActiveError } from '@/lib/external-db/kill-switch-client'; | ||
| import type { KillSwitchActiveError } from '@/lib/external-db/kill-switch-client'; | ||
| import { isSupabaseLighthousePlaceholder } from '@/lib/env/supabase-placeholder'; |
There was a problem hiding this comment.
Fix unresolved
supabase-placeholder import
The new import @/lib/env/supabase-placeholder points to a module that does not exist in this repository (src/lib/env/supabase-placeholder.{ts,tsx} is absent), so bundling/type-checking this file will fail as soon as it is included. Because this hook is part of app runtime code, the missing module blocks builds rather than being a recoverable runtime path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/security/SecurityDashboard.tsx (1)
79-87:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdicione tratamento de erro para a promise.
A consulta do Supabase usa
.then()sem.catch(). Se a query falhar, o erro será silencioso e o estadouserspermanecerá vazio sem feedback ao usuário.🛡️ Correção sugerida
supabase .from('profiles') .select('user_id, full_name, email') .eq('is_active', true) .order('full_name') .then(({ data }) => { if (data) setUsers(data.filter((u): u is typeof u & { user_id: string } => u.user_id !== null)); - }); + }) + .catch((error) => { + console.error('Erro ao carregar usuários:', error); + });Como per diretrizes de codificação: Promises sem await ou .catch() devem ser evitadas.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/security/SecurityDashboard.tsx` around lines 79 - 87, The Supabase query in SecurityDashboard.tsx currently calls supabase.from(...).select(...).then(...) without error handling; update the fetch so failures are caught and surfaced: either convert the chain to an async function (e.g., the component effect that calls the query) and wrap the await supabase.from(...).select(...) in try/catch, or append a .catch(err => ...) to the promise, and in the catch log the error (console.error or process_logger) and set a visible error state (or show a toast) instead of silently leaving users empty; target the block containing supabase.from(...).select(...).eq(...).order(...).then(...) and the setUsers call when implementing this.src/components/expert/chat/useExpertChat.ts (1)
468-526:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdicione timeout automático na chamada de streaming.
Hoje o request depende de resposta do servidor ou ação manual em “parar”. Se a conexão ficar pendurada, a UI pode ficar em loading por tempo indefinido.
💡 Ajuste sugerido
const abortController = new AbortController(); abortControllerRef.current = abortController; +const timeoutId = setTimeout(() => abortController.abort(), 45000); try { const response = await fetch( @@ ); @@ } catch (error) { @@ } finally { + clearTimeout(timeoutId); setIsLoading(false); abortControllerRef.current = null; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/expert/chat/useExpertChat.ts` around lines 468 - 526, A chamada fetch para a função expert-chat pode ficar pendurada; wrappe a AbortController existente (abortController / abortControllerRef) com um timeout: antes do fetch, criar um timeout ID que chama abortController.abort() após um valor constante (ex: STREAM_TIMEOUT_MS), passar abortController.signal para fetch (já feito) e no bloco finally do try/catch limpar o timeout (clearTimeout) e resetar abortControllerRef.current; também garanta que a ação de “parar” cancele esse timeout quando o usuário abortar manualmente. Use nomes existentes abortController, abortControllerRef, e o fetch que chama expert-chat para localizar onde inserir o setTimeout/clearTimeout e o STREAM_TIMEOUT_MS.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/admin/security/keys/useMcpKeys.ts`:
- Around line 249-255: The fire-and-forget IIFE passed into handleStepUpError
can produce unhandled promise rejections if requestStepUp() or attempt() reject;
wrap the async work in a try/catch inside the IIFE (or append a .catch()) and
surface failures via processLogger/error logging and a user-facing toast (or
existing UX feedback helper) so errors from requestStepUp() and attempt() are
caught and reported; locate the invocation passed to handleStepUpError and add
the error handling around the IIFE that calls requestStepUp and attempt.
In `@src/components/admin/security/role-migration/RoleMigrationPanel.tsx`:
- Around line 409-413: O clique do botão chama refreshBatches() com "void
refreshBatches()" sem tratamento, o que pode gerar rejection não tratado; altere
o handler do onClick para capturar erros (por exemplo usar async handler com
try/catch ou encadear .catch()) de modo que falhas em refreshBatches sejam
tratadas e exibam feedback ao usuário via state/notification; referências:
Button onClick prop, a função refreshBatches e o flag loadingBatches — assegure
também que o estado de loadingBatches é corretamente atualizado no caminho de
erro.
In `@src/components/admin/SellerDiscountLimitsPanel.tsx`:
- Around line 39-40: In SellerDiscountLimitsPanel, remove the unsafe casts (the
".from('seller_discount_limits' as never)" and the "(supabase as any)" used for
upsert) and restore type safety by declaring a typed row/interface (e.g.,
SellerDiscountLimit with user_id and max_discount_percent) and using the
Supabase client's generics: call
supabase.from<SellerDiscountLimit>('seller_discount_limits') and perform upsert
with the correctly typed payload via the properly typed supabase client instance
instead of casting to any; ensure the local payload variable matches the
SellerDiscountLimit shape so TypeScript will validate reads and writes.
In `@src/components/admin/users/useUserManagement.ts`:
- Around line 201-221: In both handleAvatarUpload and handleRemoveAvatar,
capture and check the result of the Supabase update call (the object containing
{ data, error }) instead of ignoring it; if error is truthy, log/toast the error
and abort (do not call setUsers or toast.success or return a successful value),
otherwise proceed to update local state and show success. Specifically, after
calling supabase.from('profiles').update(...).eq('user_id', userId) in
handleAvatarUpload and handleRemoveAvatar, inspect the returned { error } (and
optionally { data }) and branch on it—handle the error path by
console.error/toast.error and return null/false, and only update setUsers and
call toast.success when error is null.
In `@src/components/dashboard/MyClientsWidget.tsx`:
- Around line 160-167: The current fallback of updated_at to an empty string in
MyClientsWidget (see allQ and allO) causes incorrect lexicographic comparisons
in aggregate (the comparison at row.updated_at > r.lastInteraction); change the
fallback to a valid sentinel ISO timestamp (e.g., '1970-01-01T00:00:00Z') or
alternatively filter out items with null/undefined updated_at before building
allQ/allO so aggregate's lastInteraction comparisons use proper timestamps;
update the mapping for allQ and allO (and any related logic) to use the sentinel
or filter approach consistently.
In `@src/components/dashboard/MyRecentQuotesWidget.tsx`:
- Line 139: O texto da badge pode ficar vazio quando q.status é nulo; ajuste a
expressão usada em MyRecentQuotesWidget (onde atualmente usa (q.status ?
STATUS_LABELS[q.status] : undefined) ?? q.status) para fornecer um fallback
explícito — por exemplo usar STATUS_LABELS[q.status] ?? q.status ?? 'Sem status'
(ou outro rótulo apropriado) garantindo que sempre haja um texto visível na
badge quando q.status for nulo ou desconhecido.
In `@src/components/dashboard/RecentKitsWidget.tsx`:
- Line 93: In RecentKitsWidget, avoid using Date.now() as a fallback for
kit.updated_at because it makes items appear "updated now"; instead either
filter out kits where kit.updated_at is null before mapping/rendering or render
a sensible alternative string (e.g., "Data desconhecida" or "Unknown date") when
kit.updated_at is falsy; update the code around formatDistanceToNow(new
Date(kit.updated_at ?? Date.now()), ...) to check kit.updated_at first and call
formatDistanceToNow only for valid dates, or replace the expression with a
conditional that returns the fallback text for null/undefined updated_at.
In `@src/components/favorites/FavoritesTrashView.tsx`:
- Around line 21-25: A função formatDaysLeft precisa tratar strings de data
inválidas para evitar NaN na UI; dentro de formatDaysLeft (a função que recebe
expiresAt) valide a data antes de usar getTime(): crie a Date a partir de
expiresAt, verifique isNaN(date.getTime()) ou use Date.parse(expiresAt) e se for
NaN retorne 0 (ou outro valor seguro) em vez de calcular dias; apenas prossiga
com o cálculo Math.ceil(...) quando a data for válida.
In `@src/components/intelligence/TrendsHeatmap.tsx`:
- Around line 27-30: The Promise.all call in TrendsHeatmap.tsx currently only
destructures the data ({ data: views }, { data: searches }) and ignores Supabase
errors, so a failed query will silently appear as empty data; update the
Promise.all result destructuring to capture both data and error objects (e.g., {
data: views, error: viewsError } and { data: searches, error: searchesError }),
then check for either error and throw or return an early failure so the queryFn
fails fast; locate the supabase.from(...).select(...).gte(...) calls and add the
error checks (throw new Error or propagate the Supabase error) before using
views/searches.
In `@src/hooks/intelligence/useSalesHistory.ts`:
- Around line 191-197: The conversion denominator currently counts all quote_ids
from quoteItems (uniqueQuoteIds), including drafts; restrict the count to only
quotes that passed the earlier status filter (the filtered quotes set used
around Line 78, e.g. validQuotes or filteredQuotes). Change the uniqueQuoteIds
construction to build from (quoteItems || []) but only include qi.quote_id
values that exist in the set of filtered/valid quote IDs (create a Set of those
filtered quote IDs if not already present), then compute totalUniqueQuotes from
that restricted set so conversionRate uses the same status-filtered universe as
the numerator.
In `@src/hooks/products/useProductInsights.ts`:
- Around line 127-135: recentActivity may receive an empty string for quote
dates (q.created_at ?? ''), causing Invalid Date/NaN during recentActivity.sort;
update the push logic in useProductInsights where recentQuotes is iterated so
you either skip quotes without a valid created_at (e.g., if (!q.created_at)
return) or set a safe fallback timestamp (e.g., use new
Date(q.created_at).toISOString() when valid or use Date(0) / epoch as fallback)
before pushing; ensure the later sort uses only entries with valid date strings
or numeric timestamps so new Date(...) never produces NaN.
In `@src/hooks/quotes/useDiscountApproval.ts`:
- Around line 77-156: The code performs multiple Supabase writes/selects
(quotes.update in this block, the quotes.select for context,
quote_history.insert, admin_audit_log.insert, user_roles/profile selects and
workspace_notifications.insert) without checking the returned { error } and can
wrongly return success; for each Supabase call (look for .from('quotes').update,
.from('quotes').select(...).maybeSingle, .from('quote_history').insert,
.from('admin_audit_log').insert, .from('user_roles').select,
.from('profiles').select, .from('workspace_notifications').insert) validate the
response, and if an error exists throw or return a rejected error to abort the
flow (so the caller/toster won’t show success), log/report the error, and avoid
partial state; ideally replace this multi-step client-side sequence with a
single server-side transactional RPC to perform the update + history + audit +
notifications atomically.
In `@src/hooks/quotes/useQuoteVersions.ts`:
- Around line 130-136: The current flow in createNewVersion (which calls
createQuote(...)) first clears is_latest_version on existing rows with the
supabase .from('quotes').update({ is_latest_version: false }).or(...) then
inserts and later marks the new record as latest, exposing a race/failure
window; instead refactor so the three steps (insert new quote via createQuote,
set all existing is_latest_version = false, set the new row is_latest_version =
true) run inside a single atomic database operation (either a Postgres
transaction implemented in a server-side RPC / stored procedure or a Supabase
RPC that performs BEGIN/INSERT/UPDATE/COMMIT) so failures rollback; update
references to createNewVersion, createQuote and the existing
.from('quotes').update(...) logic to call that RPC/transaction, return the new
record or null on failure, and preserve RLS/ownership checks inside the RPC or
perform necessary checks before starting the transaction.
In `@src/hooks/simulation/useSimulation.ts`:
- Line 209: The current double-cast at savedSimulations hides a shape mismatch
between DB rows and the domain SavedSimulation and can cause runtime undefineds
(see savedSimulations and loadSavedSimulation); replace the "as unknown as
SavedSimulation[]" cast with an explicit mapping/validation step that converts
each personalization_simulations row (with product_id, product_unit_price,
simulation_data, etc.) into the domain SavedSimulation shape (id, name, options,
totalPrice, ...) or declare/use a specific DB row type and then map fields into
SavedSimulation before returning/exposing savedSimulations; ensure
missing/nullable DB fields are handled (defaults or validation) so
loadSavedSimulation consumes a correct domain object.
---
Outside diff comments:
In `@src/components/expert/chat/useExpertChat.ts`:
- Around line 468-526: A chamada fetch para a função expert-chat pode ficar
pendurada; wrappe a AbortController existente (abortController /
abortControllerRef) com um timeout: antes do fetch, criar um timeout ID que
chama abortController.abort() após um valor constante (ex: STREAM_TIMEOUT_MS),
passar abortController.signal para fetch (já feito) e no bloco finally do
try/catch limpar o timeout (clearTimeout) e resetar abortControllerRef.current;
também garanta que a ação de “parar” cancele esse timeout quando o usuário
abortar manualmente. Use nomes existentes abortController, abortControllerRef, e
o fetch que chama expert-chat para localizar onde inserir o
setTimeout/clearTimeout e o STREAM_TIMEOUT_MS.
In `@src/components/security/SecurityDashboard.tsx`:
- Around line 79-87: The Supabase query in SecurityDashboard.tsx currently calls
supabase.from(...).select(...).then(...) without error handling; update the
fetch so failures are caught and surfaced: either convert the chain to an async
function (e.g., the component effect that calls the query) and wrap the await
supabase.from(...).select(...) in try/catch, or append a .catch(err => ...) to
the promise, and in the catch log the error (console.error or process_logger)
and set a visible error state (or show a toast) instead of silently leaving
users empty; target the block containing
supabase.from(...).select(...).eq(...).order(...).then(...) and the setUsers
call when implementing this.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 55393635-d643-40c6-af0f-a7b6397407e6
📒 Files selected for processing (40)
.eslint-baseline.jsonsrc/components/admin/SellerDiscountLimitsPanel.tsxsrc/components/admin/security/ActiveIpsList.tsxsrc/components/admin/security/RecentAuditTable.tsxsrc/components/admin/security/keys/audit/useMcpAuditFeed.tssrc/components/admin/security/keys/audit/useStepUpAttempts.tssrc/components/admin/security/keys/useMcpKeys.tssrc/components/admin/security/role-migration/RoleMigrationPanel.tsxsrc/components/admin/users/RoleAuditLogPanel.tsxsrc/components/admin/users/useUserManagement.tssrc/components/dashboard/MyClientsWidget.tsxsrc/components/dashboard/MyRecentQuotesWidget.tsxsrc/components/dashboard/RecentKitsWidget.tsxsrc/components/expert/chat/useExpertChat.tssrc/components/favorites/FavoritesTrashView.tsxsrc/components/filters/FilterPresets.tssrc/components/intelligence/TopCategoriesCard.tsxsrc/components/intelligence/TrendsHeatmap.tsxsrc/components/search/useGlobalSearch.tssrc/components/security/SecurityDashboard.tsxsrc/hooks/intelligence/useSalesGoals.tssrc/hooks/intelligence/useSalesHistory.tssrc/hooks/intelligence/useSalesHistoryMacro.tssrc/hooks/kit-builder/useKitCollaboration.tssrc/hooks/products/useProductAnalytics.tssrc/hooks/products/useProductInsights.tssrc/hooks/quotes/quoteHelpers.tssrc/hooks/quotes/useDiscountApproval.tssrc/hooks/quotes/useQuoteTemplates.tssrc/hooks/quotes/useQuoteVersions.tssrc/hooks/simulation/useSimulation.tssrc/hooks/useKillSwitchBanner.tssrc/integrations/supabase/types.tssrc/lib/external-db/__tests__/kill-switch-client.test.tssrc/lib/external-db/bridge-status-events.tssrc/lib/external-db/kill-switch-client.tssrc/lib/external-db/kill-switch-telemetry.tssrc/pages/admin/SellerDiscountLimitsAdminPage.tsxsrc/pages/kit-builder/useKitBuilderQuote.tssupabase/migrations/20260525020848_restore_set_quote_number_trigger.sql
| if ( | ||
| handleStepUpError(data, error, () => { | ||
| void (async () => { | ||
| const fresh = await requestStepUp(); | ||
| if (fresh) await attempt(fresh); | ||
| })(); | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP "void\s*\(async\s*\(\)\s*=>\s*\{" src/components/admin/security/keys/useMcpKeys.ts -n -A8 -B4Repository: adm01-debug/promo-gifts-v4
Length of output: 530
Trate a rejeição no retry assíncrono do step-up
No callback passado ao handleStepUpError (linhas ~251-254), a Promise é disparada como fire-and-forget (void (async () => { ... })();) sem .catch()/try-catch; se requestStepUp() ou attempt() rejeitarem, vira unhandled rejection e perde observabilidade/feedback.
Adicione tratamento de erro no bloco (ex.: }).catch(...) ou try/catch na IIFE) com logging e toast/feedback de UX.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/security/keys/useMcpKeys.ts` around lines 249 - 255, The
fire-and-forget IIFE passed into handleStepUpError can produce unhandled promise
rejections if requestStepUp() or attempt() reject; wrap the async work in a
try/catch inside the IIFE (or append a .catch()) and surface failures via
processLogger/error logging and a user-facing toast (or existing UX feedback
helper) so errors from requestStepUp() and attempt() are caught and reported;
locate the invocation passed to handleStepUpError and add the error handling
around the IIFE that calls requestStepUp and attempt.
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={() => void refreshBatches()} | ||
| disabled={loadingBatches} |
There was a problem hiding this comment.
Trate erro da atualização manual de lotes.
Na Line 412, void refreshBatches() pode rejeitar sem tratamento. Isso pode virar unhandled rejection e esconder erro do usuário.
As per coding guidelines, "Promises sem await ou .catch()".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/security/role-migration/RoleMigrationPanel.tsx` around
lines 409 - 413, O clique do botão chama refreshBatches() com "void
refreshBatches()" sem tratamento, o que pode gerar rejection não tratado; altere
o handler do onClick para capturar erros (por exemplo usar async handler com
try/catch ou encadear .catch()) de modo que falhas em refreshBatches sejam
tratadas e exibam feedback ao usuário via state/notification; referências:
Button onClick prop, a função refreshBatches e o flag loadingBatches — assegure
também que o estado de loadingBatches é corretamente atualizado no caminho de
erro.
| .from('seller_discount_limits' as never) | ||
| .select('user_id, max_discount_percent') |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the exact file
fd -i "SellerDiscountLimitsPanel.tsx" .
# Print relevant sections with line numbers
FILE="$(fd -i "SellerDiscountLimitsPanel.tsx" . | head -n 1)"
echo "Using file: $FILE"
nl -ba "$FILE" | sed -n '1,140p'Repository: adm01-debug/promo-gifts-v4
Length of output: 250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="$(fd -i "SellerDiscountLimitsPanel.tsx" . | head -n 1)"
# Narrow search for casts
rg -n "as\s+(never|any)" "$FILE" || true
# Narrow search for supabase usage
rg -n "supabase|\.from\(|\.select\(|\.upsert\(|\.in\(" "$FILE"Repository: adm01-debug/promo-gifts-v4
Length of output: 740
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="./src/components/admin/SellerDiscountLimitsPanel.tsx"
# show the relevant area with line numbers (no nl in this env)
sed -n '1,120p' "$FILE" | cat -nRepository: adm01-debug/promo-gifts-v4
Length of output: 5452
Remover as never/as any das queries Supabase para voltar a ter segurança de tipos.
- Em
src/components/admin/SellerDiscountLimitsPanel.tsx, o.from('seller_discount_limits' as never)(linhas 39-40) e o(supabase as any)noupsert(linhas 63-68) desativam garantias de tipo justamente onde o schema/payload são usados — isso abre margem pra regressões silenciosas.
💡 Sugestão de ajuste
- const { data: limits } = await supabase
- .from('seller_discount_limits' as never)
+ const { data: limits } = await supabase
+ .from('seller_discount_limits')
.select('user_id, max_discount_percent')
.in('user_id', ids);
- // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
- const { error } = await (supabase as any)
- .from('seller_discount_limits')
+ const { error } = await supabase
+ .from('seller_discount_limits')
.upsert(
{ user_id: userId, max_discount_percent: percent, set_by: u.user.id },
{ onConflict: 'user_id' },
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/SellerDiscountLimitsPanel.tsx` around lines 39 - 40, In
SellerDiscountLimitsPanel, remove the unsafe casts (the
".from('seller_discount_limits' as never)" and the "(supabase as any)" used for
upsert) and restore type safety by declaring a typed row/interface (e.g.,
SellerDiscountLimit with user_id and max_discount_percent) and using the
Supabase client's generics: call
supabase.from<SellerDiscountLimit>('seller_discount_limits') and perform upsert
with the correctly typed payload via the properly typed supabase client instance
instead of casting to any; ensure the local payload variable matches the
SellerDiscountLimit shape so TypeScript will validate reads and writes.
| const { data: urlData } = supabase.storage.from('avatars').getPublicUrl(filePath); | ||
| const publicUrl = urlData.publicUrl + `?t=${Date.now()}`; | ||
| await supabase.from("profiles").update({ avatar_url: publicUrl }).eq("user_id", userId); | ||
| setUsers((prev) => prev.map((u) => u.user_id === userId ? { ...u, avatar_url: publicUrl } : u)); | ||
| toast.success("Foto atualizada com sucesso"); | ||
| await supabase.from('profiles').update({ avatar_url: publicUrl }).eq('user_id', userId); | ||
| setUsers((prev) => | ||
| prev.map((u) => (u.user_id === userId ? { ...u, avatar_url: publicUrl } : u)), | ||
| ); | ||
| toast.success('Foto atualizada com sucesso'); | ||
| return publicUrl; | ||
| } catch (error: unknown) { | ||
| console.error("Error uploading avatar:", error); | ||
| toast.error("Erro ao enviar foto", { description: error instanceof Error ? error.message : String(error) }); | ||
| console.error('Error uploading avatar:', error); | ||
| toast.error('Erro ao enviar foto', { | ||
| description: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| const handleRemoveAvatar = async (userId: string) => { | ||
| try { | ||
| await supabase.from("profiles").update({ avatar_url: null }).eq("user_id", userId); | ||
| setUsers((prev) => prev.map((u) => u.user_id === userId ? { ...u, avatar_url: null } : u)); | ||
| toast.success("Foto removida"); | ||
| await supabase.from('profiles').update({ avatar_url: null }).eq('user_id', userId); | ||
| setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, avatar_url: null } : u))); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP "from\('profiles'\)\.update\(\{ avatar_url" src/components/admin/users/useUserManagement.ts -n -A3 -B2Repository: adm01-debug/promo-gifts-v4
Length of output: 804
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Verificar como o Supabase client é criado (throwOnError/ opções equivalentes)
fd -a -t f "client.ts" src/integrations/supabase -0 2>/dev/null | xargs -I{} sh -c 'echo "---- {}"; sed -n "1,200p" "{}"' || true
rg -n "createClient|throwOnError" src/integrations/supabase -S || true
# 2) Ler o bloco completo das funções no arquivo alvo (contexto além das linhas 201-221)
sed -n "160,260p" src/components/admin/users/useUserManagement.ts
# 3) Confirmar se há checagem de { error } em algum ponto próximo
rg -n "{\\s*error\\s*}" src/components/admin/users/useUserManagement.ts -n -S || trueRepository: adm01-debug/promo-gifts-v4
Length of output: 3397
Checar { error } no update de profiles antes de mostrar sucesso (avatar)
- Em
handleAvatarUpload(≈ linha 203) ehandleRemoveAvatar(≈ linha 220), o resultado dosupabase.from('profiles').update(...).eq(...)é ignorado (não captura{ error }). Se a persistência falhar,setUsers/toast.successpodem refletir estado que não foi gravado.
💡 Sugestão de ajuste
- await supabase.from('profiles').update({ avatar_url: publicUrl }).eq('user_id', userId);
+ const { error: profileUpdateError } = await supabase
+ .from('profiles')
+ .update({ avatar_url: publicUrl })
+ .eq('user_id', userId);
+ if (profileUpdateError) throw profileUpdateError;
setUsers((prev) =>
prev.map((u) => (u.user_id === userId ? { ...u, avatar_url: publicUrl } : u)),
);
toast.success('Foto atualizada com sucesso');
@@
- await supabase.from('profiles').update({ avatar_url: null }).eq('user_id', userId);
+ const { error } = await supabase
+ .from('profiles')
+ .update({ avatar_url: null })
+ .eq('user_id', userId);
+ if (error) throw error;
setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, avatar_url: null } : u)));
toast.success('Foto removida');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/users/useUserManagement.ts` around lines 201 - 221, In
both handleAvatarUpload and handleRemoveAvatar, capture and check the result of
the Supabase update call (the object containing { data, error }) instead of
ignoring it; if error is truthy, log/toast the error and abort (do not call
setUsers or toast.success or return a successful value), otherwise proceed to
update local state and show success. Specifically, after calling
supabase.from('profiles').update(...).eq('user_id', userId) in
handleAvatarUpload and handleRemoveAvatar, inspect the returned { error } (and
optionally { data }) and branch on it—handle the error path by
console.error/toast.error and return null/false, and only update setUsers and
call toast.success when error is null.
| const allQ = (data?.pages.flatMap((p) => p.quotes) ?? []).map((q) => ({ | ||
| ...q, | ||
| updated_at: q.updated_at ?? '', | ||
| })); | ||
| const allO = (data?.pages.flatMap((p) => p.orders) ?? []).map((o) => ({ | ||
| ...o, | ||
| updated_at: o.updated_at ?? '', | ||
| })); |
There was a problem hiding this comment.
Fallback de updated_at para string vazia causa ordenação incorreta.
Normalizar updated_at nulo para '' (linha 162, 166) quebra a lógica de comparação na função aggregate:
- Linha 103:
if (row.updated_at > r.lastInteraction)compara strings - String vazia
''é lexicograficamente menor que timestamps ISO, fazendo registros sem data aparecerem como muito antigos - Isso pode resultar em clientes ordenados incorretamente por
lastInteraction
Use um timestamp sentinela válido (ex: '1970-01-01T00:00:00Z') ou filtre registros sem updated_at antes de agregar.
🔧 Correção sugerida
const clients = useMemo(() => {
const allQ = (data?.pages.flatMap((p) => p.quotes) ?? []).map((q) => ({
...q,
- updated_at: q.updated_at ?? '',
+ updated_at: q.updated_at ?? '1970-01-01T00:00:00Z',
}));
const allO = (data?.pages.flatMap((p) => p.orders) ?? []).map((o) => ({
...o,
- updated_at: o.updated_at ?? '',
+ updated_at: o.updated_at ?? '1970-01-01T00:00:00Z',
}));
return aggregate(allQ, allO);
}, [data]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/dashboard/MyClientsWidget.tsx` around lines 160 - 167, The
current fallback of updated_at to an empty string in MyClientsWidget (see allQ
and allO) causes incorrect lexicographic comparisons in aggregate (the
comparison at row.updated_at > r.lastInteraction); change the fallback to a
valid sentinel ISO timestamp (e.g., '1970-01-01T00:00:00Z') or alternatively
filter out items with null/undefined updated_at before building allQ/allO so
aggregate's lastInteraction comparisons use proper timestamps; update the
mapping for allQ and allO (and any related logic) to use the sentinel or filter
approach consistently.
| // B13 fix: conversion rate uses unique DOCUMENT counts, not item counts | ||
| const uniqueQuoteIds = new Set((quoteItems || []).map(qi => qi.quote_id)); | ||
| const uniqueOrderIds = new Set((orderItems || []).filter(oi => oi.order_id).map(oi => oi.order_id)); | ||
| const uniqueQuoteIds = new Set((quoteItems || []).map((qi) => qi.quote_id)); | ||
| const uniqueOrderIds = new Set( | ||
| (orderItems || []).filter((oi) => oi.order_id).map((oi) => oi.order_id), | ||
| ); | ||
| const totalUniqueQuotes = uniqueQuoteIds.size; | ||
| const totalUniqueOrders = uniqueOrderIds.size; |
There was a problem hiding this comment.
Conversão está contando quotes fora do filtro de status.
Na Line 78 você restringe quotes válidos por status, mas na Line 192 o denominador usa todos os quote_id de quote_items (incluindo drafts). Isso subestima conversionRate e quebra KPI.
💡 Ajuste sugerido
-const uniqueQuoteIds = new Set((quoteItems || []).map((qi) => qi.quote_id));
+const uniqueQuoteIds = new Set(
+ (quoteItems || [])
+ .map((qi) => qi.quote_id)
+ .filter((id): id is string => Boolean(id && quotesMap[id])),
+);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/intelligence/useSalesHistory.ts` around lines 191 - 197, The
conversion denominator currently counts all quote_ids from quoteItems
(uniqueQuoteIds), including drafts; restrict the count to only quotes that
passed the earlier status filter (the filtered quotes set used around Line 78,
e.g. validQuotes or filteredQuotes). Change the uniqueQuoteIds construction to
build from (quoteItems || []) but only include qi.quote_id values that exist in
the set of filtered/valid quote IDs (create a Set of those filtered quote IDs if
not already present), then compute totalUniqueQuotes from that restricted set so
conversionRate uses the same status-filtered universe as the numerator.
| (recentQuotes || []).forEach((q) => { | ||
| recentActivity.push({ | ||
| type: 'quote', | ||
| date: q.created_at, | ||
| details: `Adicionado em cotação (${q.quantity} un.)` | ||
| date: q.created_at ?? '', | ||
| details: `Adicionado em cotação (${q.quantity} un.)`, | ||
| }); | ||
| }); | ||
|
|
||
| recentActivity.sort((a, b) => | ||
| new Date(b.date).getTime() - new Date(a.date).getTime() | ||
| ); | ||
| recentActivity.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP "date:\s*q\.created_at\s*\?\?\s*''|recentActivity\.sort\(" src/hooks/products/useProductInsights.tsRepository: adm01-debug/promo-gifts-v4
Length of output: 208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Mostrar contexto do trecho
nl -ba src/hooks/products/useProductInsights.ts | sed -n '105,160p'
# Buscar declaração do recentActivity e de onde vem recentQuotes/q
rg -n "recentActivity" src/hooks/products/useProductInsights.ts
rg -n "recentQuotes|created_at" src/hooks/products/useProductInsights.tsRepository: adm01-debug/promo-gifts-v4
Length of output: 113
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Contexto do trecho (sem nl)
cat -n src/hooks/products/useProductInsights.ts | sed -n '120,160p'
# Buscar tipagem/definições relacionadas
rg -n "recentActivity" src/hooks/products/useProductInsights.ts
rg -n "recentQuotes|created_at" src/hooks/products/useProductInsights.tsRepository: adm01-debug/promo-gifts-v4
Length of output: 1967
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Localizar a definição do type/interface ProductInsight
rg -n "type ProductInsight|interface ProductInsight|ProductInsight\\[" src/hooks/products/useProductInsights.ts
# Mostrar o topo do arquivo (provável definição do ProductInsight)
cat -n src/hooks/products/useProductInsights.ts | sed -n '1,90p'
# Mostrar a tipagem exata de recentActivity dentro do ProductInsight
rg -n "recentActivity:\s*Array" -n src/hooks/products/useProductInsights.tsRepository: adm01-debug/promo-gifts-v4
Length of output: 3977
Evite date vazia em recentActivity de cotações.
O fallback q.created_at ?? '' (Line 130) pode gerar Invalid Date/NaN no sort (Line 135), deixando a ordenação instável.
💡 Sugestão de ajuste
- (recentQuotes || []).forEach((q) => {
+ (recentQuotes || []).forEach((q) => {
+ if (!q.created_at) return;
recentActivity.push({
type: 'quote',
- date: q.created_at ?? '',
+ date: q.created_at,
details: `Adicionado em cotação (${q.quantity} un.)`,
});
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| (recentQuotes || []).forEach((q) => { | |
| recentActivity.push({ | |
| type: 'quote', | |
| date: q.created_at, | |
| details: `Adicionado em cotação (${q.quantity} un.)` | |
| date: q.created_at ?? '', | |
| details: `Adicionado em cotação (${q.quantity} un.)`, | |
| }); | |
| }); | |
| recentActivity.sort((a, b) => | |
| new Date(b.date).getTime() - new Date(a.date).getTime() | |
| ); | |
| recentActivity.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); | |
| (recentQuotes || []).forEach((q) => { | |
| if (!q.created_at) return; | |
| recentActivity.push({ | |
| type: 'quote', | |
| date: q.created_at, | |
| details: `Adicionado em cotação (${q.quantity} un.)`, | |
| }); | |
| }); | |
| recentActivity.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/products/useProductInsights.ts` around lines 127 - 135,
recentActivity may receive an empty string for quote dates (q.created_at ?? ''),
causing Invalid Date/NaN during recentActivity.sort; update the push logic in
useProductInsights where recentQuotes is iterated so you either skip quotes
without a valid created_at (e.g., if (!q.created_at) return) or set a safe
fallback timestamp (e.g., use new Date(q.created_at).toISOString() when valid or
use Date(0) / epoch as fallback) before pushing; ensure the later sort uses only
entries with valid date strings or numeric timestamps so new Date(...) never
produces NaN.
| // Set quote status to pending_approval so UI shows correct state | ||
| await supabase | ||
| // rls-allow: fluxo de aprovação admin/seller; RLS filtra por papel | ||
| .from('quotes') | ||
| .update({ status: 'pending_approval' }) | ||
| .eq('id', quoteId); | ||
|
|
||
| // Log in quote history (incluindo flag de markup) | ||
| await supabase.from("quote_history").insert({ | ||
| quote_id: quoteId, | ||
| user_id: user.id, | ||
| action: "discount_approval_requested", | ||
| description: markup > 0 | ||
| ? `Solicitação de desconto REAL ${requestedPercent.toFixed(2)}% (aparente ${apparent.toFixed(1)}% com markup +${markup.toFixed(1)}%, limite ${maxAllowedPercent}%)` | ||
| : `Solicitação de desconto de ${requestedPercent}% (limite: ${maxAllowedPercent}%)`, | ||
| field_changed: "discount", | ||
| new_value: `${requestedPercent}%`, | ||
| metadata: { | ||
| seller_notes: sellerNotes || null, | ||
| apparent_discount_percent: apparent, | ||
| real_discount_percent: requestedPercent, | ||
| negotiation_markup_percent: markup, | ||
| }, | ||
| }); | ||
| // Buscar contexto do orçamento (markup + aparente) para auditoria e história | ||
| const { data: quoteCtx } = await supabase | ||
| // rls-allow: fluxo de aprovação admin/seller; RLS filtra por papel | ||
| .from('quotes') | ||
| .select('discount_percent, negotiation_markup_percent, real_discount_percent') | ||
| .eq('id', quoteId) | ||
| .maybeSingle(); | ||
| const markup = Number(quoteCtx?.negotiation_markup_percent ?? 0); | ||
| const apparent = Number(quoteCtx?.discount_percent ?? 0); | ||
|
|
||
| // Audit trail dedicado quando há markup (visibilidade admin) | ||
| if (markup > 0) { | ||
| await supabase.from("admin_audit_log").insert({ | ||
| // Log in quote history (incluindo flag de markup) | ||
| await supabase.from('quote_history').insert({ | ||
| quote_id: quoteId, | ||
| user_id: user.id, | ||
| action: "quote_negotiation_markup_applied", | ||
| resource_type: "quote", | ||
| resource_id: quoteId, | ||
| details: { | ||
| negotiation_markup_percent: markup, | ||
| action: 'discount_approval_requested', | ||
| description: | ||
| markup > 0 | ||
| ? `Solicitação de desconto REAL ${requestedPercent.toFixed(2)}% (aparente ${apparent.toFixed(1)}% com markup +${markup.toFixed(1)}%, limite ${maxAllowedPercent}%)` | ||
| : `Solicitação de desconto de ${requestedPercent}% (limite: ${maxAllowedPercent}%)`, | ||
| field_changed: 'discount', | ||
| new_value: `${requestedPercent}%`, | ||
| metadata: { | ||
| seller_notes: sellerNotes || null, | ||
| apparent_discount_percent: apparent, | ||
| real_discount_percent: requestedPercent, | ||
| max_allowed_percent: maxAllowedPercent, | ||
| context: "discount_approval_request", | ||
| negotiation_markup_percent: markup, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| // Notify all admins | ||
| const { data: adminRoles } = await supabase | ||
| .from("user_roles") | ||
| .select("user_id") | ||
| .eq("role", "admin"); | ||
| if (adminRoles && adminRoles.length > 0) { | ||
| const { data: profile } = await supabase | ||
| .from("profiles") | ||
| .select("full_name") | ||
| .eq("user_id", user.id) | ||
| .maybeSingle(); | ||
| const sellerName = profile?.full_name || "Vendedor"; | ||
| const msg = markup > 0 | ||
| ? `${sellerName} solicitou desconto real de ${requestedPercent.toFixed(2)}% (aparente ${apparent.toFixed(1)}% com markup +${markup.toFixed(1)}%, limite ${maxAllowedPercent}%)` | ||
| : `${sellerName} solicitou ${requestedPercent.toFixed(1)}% de desconto (limite: ${maxAllowedPercent}%)`; | ||
| await supabase.from("workspace_notifications").insert( | ||
| adminRoles.map(a => ({ | ||
| user_id: a.user_id, | ||
| title: "Solicitação de desconto", | ||
| message: msg, | ||
| type: "warning", | ||
| category: "discount", | ||
| action_url: "/admin/usuarios?tab=discounts", | ||
| })) | ||
| ); | ||
| } | ||
| // Audit trail dedicado quando há markup (visibilidade admin) | ||
| if (markup > 0) { | ||
| await supabase.from('admin_audit_log').insert({ | ||
| user_id: user.id, | ||
| action: 'quote_negotiation_markup_applied', | ||
| resource_type: 'quote', | ||
| resource_id: quoteId, | ||
| details: { | ||
| negotiation_markup_percent: markup, | ||
| apparent_discount_percent: apparent, | ||
| real_discount_percent: requestedPercent, | ||
| max_allowed_percent: maxAllowedPercent, | ||
| context: 'discount_approval_request', | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| toast.success("Solicitação de aprovação enviada ao admin!"); | ||
| return true; | ||
| } catch (err) { | ||
| console.error("Error requesting approval:", err); | ||
| toast.error("Erro ao solicitar aprovação"); | ||
| return false; | ||
| } | ||
| }, [user]); | ||
| // Notify all admins | ||
| const { data: adminRoles } = await supabase | ||
| .from('user_roles') | ||
| .select('user_id') | ||
| .eq('role', 'admin'); | ||
| if (adminRoles && adminRoles.length > 0) { | ||
| const { data: profile } = await supabase | ||
| .from('profiles') | ||
| .select('full_name') | ||
| .eq('user_id', user.id) | ||
| .maybeSingle(); | ||
| const sellerName = profile?.full_name || 'Vendedor'; | ||
| const msg = | ||
| markup > 0 | ||
| ? `${sellerName} solicitou desconto real de ${requestedPercent.toFixed(2)}% (aparente ${apparent.toFixed(1)}% com markup +${markup.toFixed(1)}%, limite ${maxAllowedPercent}%)` | ||
| : `${sellerName} solicitou ${requestedPercent.toFixed(1)}% de desconto (limite: ${maxAllowedPercent}%)`; | ||
| await supabase.from('workspace_notifications').insert( | ||
| adminRoles.map((a) => ({ | ||
| user_id: a.user_id, | ||
| title: 'Solicitação de desconto', | ||
| message: msg, | ||
| type: 'warning', | ||
| category: 'discount', | ||
| action_url: '/admin/usuarios?tab=discounts', | ||
| })), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Há writes críticos sem validação de erro, com sucesso falso para o usuário.
Nestes blocos, vários update/insert são executados sem checar error (ou só com console.error), mas no final ainda retorna true e mostra toast.success. Isso pode deixar o fluxo em estado parcial (status alterado sem histórico/notificação, ou vice-versa).
Sugestão: validar error em cada write e abortar o fluxo com throw; idealmente consolidar em RPC transacional no backend.
Also applies to: 202-244
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/quotes/useDiscountApproval.ts` around lines 77 - 156, The code
performs multiple Supabase writes/selects (quotes.update in this block, the
quotes.select for context, quote_history.insert, admin_audit_log.insert,
user_roles/profile selects and workspace_notifications.insert) without checking
the returned { error } and can wrongly return success; for each Supabase call
(look for .from('quotes').update, .from('quotes').select(...).maybeSingle,
.from('quote_history').insert, .from('admin_audit_log').insert,
.from('user_roles').select, .from('profiles').select,
.from('workspace_notifications').insert) validate the response, and if an error
exists throw or return a rejected error to abort the flow (so the caller/toster
won’t show success), log/report the error, and avoid partial state; ideally
replace this multi-step client-side sequence with a single server-side
transactional RPC to perform the update + history + audit + notifications
atomically.
| // Mark all existing versions as not latest | ||
| await supabase | ||
| // rls-allow: lookup por quote_id; RLS valida ownership | ||
| .from('quotes') | ||
| .update({ is_latest_version: false }) | ||
| .or(`id.eq.${rootId},parent_quote_id.eq.${rootId}`); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/quotes/useQuoteVersions.ts"
# Ensure file exists
ls -la "$FILE"
# Show line ranges around the reported sections
echo "---- LINES 110-160 ----"
nl -ba "$FILE" | sed -n '110,160p'
echo "---- LINES 155-230 ----"
nl -ba "$FILE" | sed -n '155,230p'
# Search for any transaction/rpc usage inside this file
echo "---- SEARCH for transaction/RPC/atomic patterns ----"
rg -n --hidden --no-ignore-vcs "transaction|rpc\\(|supabase\\.(rpc|from\\().*\\.(update|insert)|begin|commit|rollback|atomic" "$FILE" || true
# Search for createNewVersion and where it performs create/update
echo "---- SEARCH createNewVersion ----"
rg -n --hidden --no-ignore-vcs "createNewVersion|is_latest_version|latest" "$FILE"Repository: adm01-debug/promo-gifts-v4
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/quotes/useQuoteVersions.ts"
ls -la "$FILE"
echo "---- LINES 110-160 ----"
cat -n "$FILE" | sed -n '110,160p'
echo "---- LINES 155-230 ----"
cat -n "$FILE" | sed -n '155,230p'
echo "---- SEARCH for transaction/RPC/atomic patterns ----"
rg -n --hidden --no-ignore-vcs "transaction|rpc\\(|supabase\\.(rpc|from\\().*\\.(update|insert)|begin|commit|rollback|atomic" "$FILE" || true
echo "---- SEARCH createNewVersion / is_latest_version ----"
rg -n --hidden --no-ignore-vcs "createNewVersion|is_latest_version|latest" "$FILE" || trueRepository: adm01-debug/promo-gifts-v4
Length of output: 6078
Risco de inconsistência: createNewVersion desmarca is_latest_version antes de garantir a nova versão
Em src/hooks/quotes/useQuoteVersions.ts (Linhas 130-136), o fluxo marca todas as versões do orçamento como is_latest_version: false antes de chamar createQuote(...) e só depois (Linhas 165-197) atualiza o registro novo com is_latest_version: true. Se createQuote ou a atualização final falhar, não há rollback/atomicidade (o catch só trata o erro e retorna null), podendo deixar a família sem nenhuma versão “latest”.
Solução: mover esse conjunto “criar nova versão + desmarcar antigas + marcar a nova como latest” para uma operação atômica no banco (transaction/RPC) para garantir consistência em caso de falha ou concorrência.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/quotes/useQuoteVersions.ts` around lines 130 - 136, The current
flow in createNewVersion (which calls createQuote(...)) first clears
is_latest_version on existing rows with the supabase .from('quotes').update({
is_latest_version: false }).or(...) then inserts and later marks the new record
as latest, exposing a race/failure window; instead refactor so the three steps
(insert new quote via createQuote, set all existing is_latest_version = false,
set the new row is_latest_version = true) run inside a single atomic database
operation (either a Postgres transaction implemented in a server-side RPC /
stored procedure or a Supabase RPC that performs BEGIN/INSERT/UPDATE/COMMIT) so
failures rollback; update references to createNewVersion, createQuote and the
existing .from('quotes').update(...) logic to call that RPC/transaction, return
the new record or null on failure, and preserve RLS/ownership checks inside the
RPC or perform necessary checks before starting the transaction.
| }, | ||
| }); | ||
| const savedSimulations = (_savedSimulations ?? []) as SavedSimulation[]; | ||
| const savedSimulations = (_savedSimulations ?? []) as unknown as SavedSimulation[]; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confere o ponto do double-cast e o contrato de SavedSimulation
rg -n "as unknown as SavedSimulation\\[]" src/hooks/simulation/useSimulation.ts
rg -n "SavedSimulation" src/types/simulation.tsRepository: adm01-debug/promo-gifts-v4
Length of output: 182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== useSimulation.ts (contexto ~180-240) ==="
sed -n '180,240p' src/hooks/simulation/useSimulation.ts | cat -n
echo "=== types de SavedSimulation (contexto) ==="
# tenta achar a definição mais provável
fd -a "simulation" src/types | head -n 20
fd -a "SavedSimulation" src/types -t f | head -n 50
rg -n "export type SavedSimulation|interface SavedSimulation|type SavedSimulation|SavedSimulation" src/types/simulation.ts src/types -S | head -n 50
# mostra o arquivo provável
if [ -f src/types/simulation.ts ]; then
sed -n '1,200p' src/types/simulation.ts | cat -n
fiRepository: adm01-debug/promo-gifts-v4
Length of output: 4045
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' src/types/domain/simulation.ts | cat -n
rg -n "export interface SavedSimulation" -n src/types/domain/simulation.ts
# mostra só o bloco do interface
python3 - <<'PY'
import re
path="src/types/domain/simulation.ts"
start_pat=re.compile(r'export interface SavedSimulation\s*{')
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
start=None
for i,l in enumerate(lines):
if start_pat.search(l):
start=i
break
if start is None:
raise SystemExit("not found")
brace=0
out=[]
for j in range(start,len(lines)):
out.append((j+1,lines[j].rstrip("\n")))
brace += lines[j].count('{')-lines[j].count('}')
if j>start and brace==0:
break
for ln,txt in out[:200]:
print(f"{ln:5d}\t{txt}")
PYRepository: adm01-debug/promo-gifts-v4
Length of output: 3062
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "savedSimulations" src/hooks/simulation/useSimulation.ts
rg -n "\.options\b|\.totalPrice\b|\.createdAt\b|\.updatedAt\b|\.status\b|\.simulation_data\b" src/hooks/simulation/useSimulation.ts
sed -n '1,220p' src/hooks/simulation/useSimulation.ts | cat -n
sed -n '220,420p' src/hooks/simulation/useSimulation.ts | cat -nRepository: adm01-debug/promo-gifts-v4
Length of output: 18011
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== src/types/simulation.ts (contexto) ==="
sed -n '1,220p' src/types/simulation.ts | cat -n
echo "=== usos de SavedSimulation no useSimulation.ts ==="
rg -n "SavedSimulation|simulation\.product_id|product_id|product_unit_price|simulation_data|quantity" src/hooks/simulation/useSimulation.ts
echo "=== trecho de loadSavedSimulation (contexto) ==="
sed -n '185,280p' src/hooks/simulation/useSimulation.ts | cat -n
sed -n '280,520p' src/hooks/simulation/useSimulation.ts | cat -nRepository: adm01-debug/promo-gifts-v4
Length of output: 17059
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "export (interface|type) SavedSimulation|interface SavedSimulation|type SavedSimulation|SavedSimulation\s*=" src/types/simulation.ts src/types -S
rg -n "product_id|product_unit_price|simulation_data|quantity" src/types/simulation.ts src/types -SRepository: adm01-debug/promo-gifts-v4
Length of output: 2441
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== src/types/simulation.ts (full) ==="
wc -l src/types/simulation.ts
sed -n '1,220p' src/types/simulation.ts | cat -n
sed -n '220,520p' src/types/simulation.ts | cat -nRepository: adm01-debug/promo-gifts-v4
Length of output: 1274
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "interface SavedSimulation|type SavedSimulation" src --glob='*.ts' --glob='*.tsx'
rg -n "product_id|product_unit_price|simulation_data|seller_id|client_id" src/types src/hooks/simulation --glob='*.ts' --glob='*.tsx'Repository: adm01-debug/promo-gifts-v4
Length of output: 3952
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la src/types/domain
rg -n "export .*SavedSimulation" src/types/domain -S --glob='*.ts'
rg -n "export type \{[^}]*SavedSimulation" src/types/domain -S --glob='*.ts'
# mostra o arquivo provável de barrel
for f in src/types/domain/index.ts src/types/domain.ts src/types/domain/index.tsx; do
if [ -f "$f" ]; then echo "=== $f ==="; sed -n '1,200p' "$f" | cat -n; fi
doneRepository: adm01-debug/promo-gifts-v4
Length of output: 509
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== src/types/domain/index.ts ==="
sed -n '1,200p' src/types/domain/index.ts | cat -n
echo "=== src/types/domain/simulation.ts (SavedSimulation block) ==="
sed -n '40,90p' src/types/domain/simulation.ts | cat -nRepository: adm01-debug/promo-gifts-v4
Length of output: 1905
Evite as unknown as e alinhe o shape de SavedSimulation ao que vem do Supabase
Em src/hooks/simulation/useSimulation.ts, o cast (_savedSimulations ?? []) as unknown as SavedSimulation[] (linha 209) mascara um shape incompatível: o SavedSimulation importado é o do domínio (id, name, options, totalPrice...), mas o hook trata o resultado como registros persistidos com product_id, product_unit_price e simulation_data (usado em loadSavedSimulation), o que pode resultar em undefined/quebras em runtime. Troque o double-cast por um mapeamento/validação explícitos do retorno de personalization_simulations (ou use um tipo específico do “row” do banco) antes de expor savedSimulations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/simulation/useSimulation.ts` at line 209, The current double-cast
at savedSimulations hides a shape mismatch between DB rows and the domain
SavedSimulation and can cause runtime undefineds (see savedSimulations and
loadSavedSimulation); replace the "as unknown as SavedSimulation[]" cast with an
explicit mapping/validation step that converts each personalization_simulations
row (with product_id, product_unit_price, simulation_data, etc.) into the domain
SavedSimulation shape (id, name, options, totalPrice, ...) or declare/use a
specific DB row type and then map fields into SavedSimulation before
returning/exposing savedSimulations; ensure missing/nullable DB fields are
handled (defaults or validation) so loadSavedSimulation consumes a correct
domain object.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
✅ Merge resolvido e push concluído via Claude Code VPS. Resultado:
Validações pós-merge no working tree:
Aguardando CI (Vercel preview, lint, build) para liberar o botão "Squash and merge". O Vercel falhou no commit anterior ( Lembrete antes do merge final (já estava na descrição do PR mas vale repetir): a migration |
…ma (#338) Same bug as #329 fixed in useProductAnalytics.ts but missed in useGlobalSearch.ts. Insert was sending columns that do not exist in public.search_analytics: - seller_id → user_id (column name mismatch) - filters_used → search_context (column does not exist; the schema has search_context text) Validated against live DB (doufsxqlfjyuvxuezpln): search_analytics columns are id, user_id, search_term, results_count, search_context, created_at — no seller_id, no filters_used. Without this fix every global search insert was rejected with PGRST204 (column not found), silently breaking search telemetry. The latency+intent payload is now serialized as JSON into search_context (text) since the new column type is text, not jsonb.
Commits b4946b6 (#329) e 972f7d5 (#269) adicionaram novos arquivos com padrões de toast.error(err.message) após a última geração do baseline (2026-05-23). Arquivos afetados: - src/components/admin/security/keys/useMcpKeys.ts - src/components/admin/security/role-migration/RoleMigrationPanel.tsx - src/components/admin/users/useUserManagement.ts São dívidas técnicas pré-existentes — não introduzidas por este PR. O gate passa com 101 ocorrências legadas e 0 novas. https://claude.ai/code/session_014GFa1o253ZufoW69aqKdku
Auditoria pós-merge do #329 identificou 3 campos referenciados pelo front que não existem no banco vivo (doufsxqlfjyuvxuezpln): - is_personalizable em product_group_components - area_image_url em product_*_locations - is_default em product_*_location_techniques Decisão de produto (A=limpar / B=ativar via RPC) está pendente — owner de produto (Pink) precisa avaliar roadmap de personalização. Mudança puramente cosmética: - Adiciona JSDoc @deprecated TODO(#339) nos 10 pontos onde os 3 campos aparecem em interfaces TypeScript / mutation types - Mantém todo o comportamento existente (UI, mutations, mappers) - Faz aparecer warning no editor quando alguém tocar nesses campos, apontando para o issue com o contexto completo Drive-by fixes para passar baseline gates do pre-push: - ProductPersonalizationRules: prefixa productId não-usado com _ para satisfazer no-unused-vars (erro pre-existente em main) - SidebarNavGroup: troca `item.badge != null` por `item.badge !== undefined && item.badge !== null` para satisfazer eqeqeq (preserva semântica exata) NÃO afeta runtime. NÃO altera schema. NÃO altera contratos com Supabase. Refs: #339 Co-authored-by: adm01-debug <ti@promobrindes.com.br>
… coverage por módulo (#331) * feat(tests): suíte E2E + integração de edge functions + fuzz expandido + coverage por módulo - /produtos: 7 cenários (lista, busca, filtro, estoque zero, empty state, paginação, @mobile) - /montar-kit: 7 cenários (happy, adicionar item, quantidade, kit vazio, AI offline, @mobile, total) - /mockup-generator: 6 cenários (happy, IA 500, timeout 504, histórico vazio, @mobile) - /orcamentos/novo: 8 cenários (render, cliente, CNPJ, validação, erro 400, 503, produto, @mobile) tests/edge-functions/integration/: - health-check: status healthy/degraded/unhealthy, sem stack trace, CORS, método OPTIONS - cnpj-lookup: 7 categorias × N cenários (happy, 400, 401, CB, 404, 422, CORS) - webhook-inbound: v1/v2, HMAC, idempotência, fuzz básico, rate-limit, CORS - secure-upload: upload válido, SHA-256, tipos inválidos, 413, antivírus, 401, CORS - send-notification: canais (in-app/email/push), campos obrigatórios, 401, 404, fuzz - validate-access: RBAC allow/deny, auth, payload inválido, inputs extremos - generate-mockup: happy, posições, validação, SSRF, IA timeout, 404, CORS - quote-sync: CRM sync, idempotência, validação, 404, CRM offline, valores extremos - Corpus: SQL injection (10), XSS (9), path traversal/SSRF (9), huge strings (7), type confusion (8), malformed JSON (7), CNPJ inválidos (6), datas inválidas (5), valores numéricos extremos (8) - 8 funções alvo: cnpj-lookup, product-webhook, webhook-inbound, secure-upload, send-notification, validate-access, generate-mockup, external-db-bridge - 252 payloads em dry-run; modo LIVE faz HTTP real com detecção de 500/stack-leak - Dry-run automático quando credenciais ausentes (CI sem secrets) scripts/generate-coverage-report.mjs: - Lê coverage/coverage-summary.json e gera: coverage/module-coverage-report.json (por módulo: hooks, pages, components, utils, lib) coverage/route-coverage-report.json (por rota: /produtos, /orcamentos, /admin, etc.) coverage/coverage-report.md (Markdown com tabelas e ícones ✅/⚠️ /❌) - --check mode falha com exit 1 se algum módulo abaixo do threshold - Integrado ao CI no job 'coverage' e 'integration-tests' como step informativo - Job 'integration-tests': roda os novos testes de integração mocked antes do fuzz - Job 'coverage': adiciona step de relatório por módulo/rota após coverage run - Artifacts publicados: module-coverage-report-$RUN_ID com JSON + Markdown https://claude.ai/code/session_014GFa1o253ZufoW69aqKdku * fix(ci): atualiza baselines TS/ESLint para erros pré-existentes em src/ + remove vi unused em health-check.test.ts Baselines defasados (27873bf) após 19 commits que tocaram src/ sem atualizar. Novos erros no baseline: - src/lib/external-db/__tests__/kill-switch-client.test.ts: TS2556 +1 - src/lib/external-db/invoke.ts: TS2353 +2 - src/lib/external-db/kill-switch-telemetry.ts: no-explicit-any +1, naming-convention +1 - src/pages/admin/ObservabilityDashboard.tsx: eqeqeq +1 Todos pré-existentes — nenhum introduzido por esta branch. Fix de lint em health-check.test.ts: remove import `vi` não-utilizado e variável `fetchMock` declarada no describe mas sempre re-criada nos testes. https://claude.ai/code/session_014GFa1o253ZufoW69aqKdku * fix(ci): patch eslint baseline para 2 eqeqeq drift em SidebarNavGroup.tsx * fix(rls): move rls-allow annotations para linha do .from() — seller-scope checker O checker verifica apenas lines[idx] (mesma linha do .from()) ou lines[idx-1] (linha imediatamente anterior). As anotações // rls-allow estavam 2 linhas acima do .from(), interpostas pela linha com 'supabase', e por isso eram ignoradas pelo scanner. Arquivos afetados (introduzidos pelo rebase sobre main): - src/hooks/intelligence/useSalesHistoryMacro.ts - src/hooks/quotes/useDiscountApproval.ts Lógica de scope preservada — ambas as queries têm justificativa legítima (filtro explícito por id já no payload / fluxo admin de aprovação de desconto). https://claude.ai/code/session_014GFa1o253ZufoW69aqKdku * fix(rls): add rls-allow annotation em SidebarReorganized.tsx — seller-scope checker Commit aa44ec9 em main refatorou SidebarReorganized para corrigir infinite render loop. A versão nova inclui .from('discount_approval_requests') sem a anotação // rls-allow que existia antes da refatoração. O PR faz merge sobre main, então o CI avalia o merge commit, que tem a versão do arquivo sem anotação — causando falha no seller-scope checker. A query é legítima (admin-only badge com isAdmin guard), então a anotação é a correção correta. https://claude.ai/code/session_014GFa1o253ZufoW69aqKdku * fix(ci): atualiza toast-leaks-baseline para violations pós-May-23 Commits b4946b6 (#329) e 972f7d5 (#269) adicionaram novos arquivos com padrões de toast.error(err.message) após a última geração do baseline (2026-05-23). Arquivos afetados: - src/components/admin/security/keys/useMcpKeys.ts - src/components/admin/security/role-migration/RoleMigrationPanel.tsx - src/components/admin/users/useUserManagement.ts São dívidas técnicas pré-existentes — não introduzidas por este PR. O gate passa com 101 ocorrências legadas e 0 novas. https://claude.ai/code/session_014GFa1o253ZufoW69aqKdku --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: adm01-debug <ti@promobrindes.com.br>
Resumo
O frontend estava desalinhado com o Supabase (
doufsxqlfjyuvxuezpln). A causa-raiz:src/integrations/supabase/types.tsestava muito desatualizado (141/296 tabelas, 1/115 views, 65/787 funções), mascarando divergências reais de schema. Este PR regenera os tipos a partir do banco vivo e corrige cada divergência que apareceu.Mudanças
types.tsdo banco vivo (141→296 tabelas, 1→115 views, 65→385 funções).src/lib/supabase-untyped.tsescripts/lint-untyped-from.shestavam commitados em base64 desde o chore: type-safety cleanup + CI lint untypedFrom (encerra follow-ups do #317) #319 (não compilavam / não executavam). Decodificados de volta para fonte válida.quotes.client_nameé NOT NULL → parou de inserirnull.quote_items.subtotalé obrigatório → passou a calcular no insert.search_analyticsusauser_id(nãoseller_id) e não temfilters_used→ insert corrigido (tracking de busca estava quebrado).quotes.quote_numberé NOT NULL sem default, e o triggerset_quote_numberque o autogerava foi perdido no drift de replay de migrations (a funçãogenerate_quote_number()continua lá). Sem ele, todo INSERT de orçamento falhava com violação de NOT NULL. Migration20260525020848_restore_set_quote_number_trigger.sqlrestaura o trigger; os inserts passamquote_number: ''para o trigger preencher.untypedFrom()→supabase.from()tipado nos arquivos de intelligence; remove castsas anyagora desnecessários no kill-switch client/telemetry.A migration
20260525020848_restore_set_quote_number_trigger.sqlprecisa ser aplicada ao banco de produção (restaura o trigger dequote_number). Sem ela, criação de orçamento permanece quebrada. Posso aplicá-la via MCP mediante aprovação.Fora de escopo / follow-ups
is_personalizable,area_image_url,is_defaultemproduct_group_*/product_component_*). Mantidos emuntypedFrom()(guardado pelo lint); requer decisão de produto (adicionar colunas ao DB vs. remover do front).cannot set parameter "role"não veio deseed_discount_test_users(falso-positivo de regex); nenhuma funçãoSECURITY DEFINERfazSET ROLEde fato.Validação
tscbaseline: 484 erros (−24 vs 508, zero regressões).eslintbaseline: limpo (sem regressões).npm run build: ✅Generated by Claude Code
Summary by cubic
Realigned the frontend with the live Supabase schema and restored quote number generation so quote creation works again. Also reconciled main’s bridge status events, kill‑switch banner/telemetry, and tests while extending the typed client migration across admin/security, intelligence, products, quotes, and expert chat.
Bug Fixes
src/integrations/supabase/types.tsfrom the live DB (141→296 tables, 1→115 views, 65→385 functions).src/lib/supabase-untyped.tsandscripts/lint-untyped-from.shfrom base64 so they compile and run.user_id, timestamps, labels, and names).quotes.client_nameis NOT NULL → set defaults and stop insertingnull(Kit Builder now sets'Sem cliente').quote_items.subtotalis required → compute on insert.search_analyticsusesuser_idand has nofilters_used→ fixed insert so tracking works.category/is_default; normalizecreated_at/updated_at.untypedFrom()with typedsupabase.from()across intelligence (Top categories, Trends heatmap), product analytics/insights, sales goals/history/macro, quotes (versions, discount approval), admin/security panels, and expert chat; removedas anycasts.kill_switch_hits.BridgeStatusEventInputfor correct union typing and consistentemitBridgeStatusinput.kill-switch-clienttests with main’s typed mocking.Migration
20260525020848_restore_set_quote_number_trigger.sqlto restore theset_quote_numberBEFORE INSERT trigger forpublic.quotes(uses existinggenerate_quote_number()).quote_number: ''so the trigger fills it.Written for commit a44f88e. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
Notas de Lançamento
New Features
Bug Fixes
Improvements