diff --git a/docs/historico-agente-desktop-2025-10-10.md b/docs/historico-agente-desktop-2025-10-10.md new file mode 100644 index 0000000..012f910 --- /dev/null +++ b/docs/historico-agente-desktop-2025-10-10.md @@ -0,0 +1,100 @@ +# Histórico — Agente Desktop (Tauri) — 2025-10-10 + +> Registro consolidado do que foi feito no app desktop, problemas encontrados, diagnósticos e próximos passos. Complementa `docs/plano-app-desktop-maquinas.md` e `apps/desktop/README.md`. + +## Resumo do que mudou +- UI mais "shadcn-like" sem Tailwind (apenas CSS): tema claro forçado, card com sombras suaves, labels fortes, helper text opcional e estados de foco com ring. +- Campo "Código de provisionamento" com botão de visibilidade (olhinho) sem dependências extras. +- Cards de inventário na visão inicial (CPU, Memória, Sistema e Discos) e grid simplificada. +- Feedback de erro aprimorado no registro: exibe status HTTP e detalhes retornados pelo servidor. +- Botão "Abrir sistema" passou a abrir o navegador padrão (plugin opener) em vez de navegar dentro da WebView. +- Corrigidas permissões do plugin Store no Tauri v2 (antes: `store.load not allowed`). + +## Arquivos alterados +- `apps/desktop/index.html:1` + - Força tema claro com `` e estrutura do card. +- `apps/desktop/src/styles.css:1` + - Estilos do card, inputs, input-group, ícones, tabs e summary cards (visual shadcn-like sem Tailwind). Remove dark overrides. +- `apps/desktop/src/main.ts:3` + - Importa `openUrl` do plugin opener e usa para abrir o handshake no navegador padrão. +- `apps/desktop/src/main.ts:640` + - Tratamento de erros no registro com exibição de `status` e `details` quando o servidor retorna JSON/texto. +- `apps/desktop/src/main.ts:694` + - Função `redirectToApp` para abrir `APP_URL/machines/handshake?token=...` via `openUrl`, com fallback para `window.location.replace`. +- `apps/desktop/src-tauri/capabilities/default.json:1` + - Adicionadas permissões: `store:default`, `store:allow-load`, `store:allow-get`, `store:allow-set`, `store:allow-save`, `store:allow-delete` e `opener:default`. + +## Como rodar (Windows, dev) +1) Garantir `.env` do desktop em `apps/desktop/.env`: +``` +VITE_APP_URL=https://tickets.esdrasrenan.com.br +VITE_API_BASE_URL=https://tickets.esdrasrenan.com.br +``` +2) Rodar dev: +``` +cd apps\desktop +pnpm tauri dev +``` +3) Provisionar: +- Usar o botão de olho para conferir o segredo, sem espaços. +- Deixar `Tenant` e `Empresa (slug)` vazios para o primeiro teste. +- Ao concluir, o app abre o navegador em `/machines/handshake?token=...`. + +Referências úteis: +- Defaults/URLs do app: `apps/desktop/src/main.ts:75` +- Handshake na web: `src/app/machines/handshake/route.ts:1` +- Endpoint de registro: `src/app/api/machines/register/route.ts:1` + +## Diagnósticos e soluções aplicadas +- Erro na Store (Tauri v2): + - Sintoma: `store.load not allowed` nos logs do DevTools. + - Causa: permissões do plugin Store ausentes. + - Ação: adicionar permissões em `apps/desktop/src-tauri/capabilities/default.json:1`. +- Erro 500 durante registro com empresa: + - Mensagem: `ConvexError: Empresa não encontrada para o tenant informado`. + - Causa: slug inválido em `Empresa (slug)`. + - Ação: validar com slug correto (Admin > Empresas & Clientes) ou registrar sem empresa. + - Observação: hoje o endpoint mapeia como 500 genérico; ver "Pendências" para remapear para 400/404. +- Redirecionando para `localhost` após registro: + - Causa: configuração antiga salva no Store (primeira tentativa em dev) ou navegação dentro da WebView. + - Ações: + - Abrir no navegador padrão com `openUrl` (`apps/desktop/src/main.ts:694`). + - Se necessário, limpar Store via botão "Reprovisionar" (Configurações) ou removendo o arquivo `machine-agent.json` no diretório de dados do app. +- Mensagem de erro genérica no desktop: + - Antes: "Erro desconhecido ao registrar a máquina". + - Agora: exibe `Falha ao registrar máquina (STATUS): mensagem — detalhes` (quando disponíveis), facilitando diagnóstico. + +## Provisionamento — segredo e boas práticas +- Variável: `MACHINE_PROVISIONING_SECRET` (VPS/Convex backend). +- Rotina de giro (secret exposto foi mostrado no chat): + 1. Gerar novo segredo (ex.: `openssl rand -hex 32`). + 2. Aplicar no serviço Convex (Swarm) e forçar redeploy: + ``` + docker service update --env-add MACHINE_PROVISIONING_SECRET='NOVO_HEX' sistema_convex_backend + docker service update --force sistema_convex_backend + ``` + 3. Validar com `POST /api/machines/register` (esperado 201). +- Máquinas já registradas não são afetadas (token delas continua válido). + +## Pendências e próximos passos +- Mapear erros "esperados" para HTTP adequado no web (Next): + - Em `src/app/api/machines/register/route.ts:1`, detectar `ConvexError` conhecidos (empresa inválida, token inválido, etc.) e responder `400`/`404` em vez de `500`. +- Validar UX do botão "Abrir sistema": + - Confirmar que sempre abre no navegador padrão em produção (capability `opener:default` já presente). +- Polimento visual adicional (opcional): + - Botões com variações de cor/hover mais fiéis ao `/login`. + - Trocar ícones emoji por SVGs minimalistas. +- Métricas de CPU no agente (suavização): + - Avaliar média de 2–3 amostras no lado Rust antes de reportar a primeira leitura (Task Manager-like). +- Documentar re-provisionamento manual do Store por SO (paths exatos) no `apps/desktop/README.md`. + +## Checklist rápido de verificação (QA) +- `.env` do desktop contém apenas `VITE_APP_URL` e `VITE_API_BASE_URL` apontando para produção. +- Primeiro registro sem empresa retorna 201 e aparece "Máquina provisionada" nas abas. +- "Ambiente" e "API" em Configurações exibem `https://tickets.esdrasrenan.com.br`. +- "Abrir sistema" abre o navegador com `/machines/handshake?token=...` e loga a máquina. +- Reprovisionar limpa a Store e volta ao formulário inicial. + +--- + +Dúvidas/sugestões: ver `agents.md` para diretrizes gerais e `docs/desktop-build.md` para build de binários. diff --git a/src/app/tickets/new/page.tsx b/src/app/tickets/new/page.tsx index ab81758..95dbf78 100644 --- a/src/app/tickets/new/page.tsx +++ b/src/app/tickets/new/page.tsx @@ -28,12 +28,13 @@ import { CategorySelectFields } from "@/components/tickets/category-select" export default function NewTicketPage() { const router = useRouter() - const { convexUserId } = useAuth() - const queueArgs = convexUserId + const { convexUserId, isStaff } = useAuth() + const queuesEnabled = Boolean(isStaff && convexUserId) + const queueArgs = queuesEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip" const queuesRaw = useQuery( - convexUserId ? api.queues.summary : "skip", + queuesEnabled ? api.queues.summary : "skip", queueArgs ) as TicketQueueSummary[] | undefined const queues = useMemo(() => queuesRaw ?? [], [queuesRaw]) diff --git a/src/components/chart-area-interactive.tsx b/src/components/chart-area-interactive.tsx index b53cd06..1e9eb49 100644 --- a/src/components/chart-area-interactive.tsx +++ b/src/components/chart-area-interactive.tsx @@ -48,7 +48,7 @@ export function ChartAreaInteractive() { // Persistir seleção de empresa globalmente const [companyId, setCompanyId] = usePersistentCompanyFilter("all") const [companyQuery, setCompanyQuery] = React.useState("") - const { session, convexUserId } = useAuth() + const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID React.useEffect(() => { @@ -61,13 +61,14 @@ export function ChartAreaInteractive() { } }, [isMobile]) + const reportsEnabled = Boolean(isStaff && convexUserId) const report = useQuery( api.reports.ticketsByChannel, - convexUserId + reportsEnabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) - const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + const companies = useQuery(api.companies.list, reportsEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined const filteredCompanies = React.useMemo(() => { const q = companyQuery.trim().toLowerCase() if (!q) return companies ?? [] diff --git a/src/components/charts/chart-opened-resolved.tsx b/src/components/charts/chart-opened-resolved.tsx index 45aeda0..4975112 100644 --- a/src/components/charts/chart-opened-resolved.tsx +++ b/src/components/charts/chart-opened-resolved.tsx @@ -25,12 +25,13 @@ const chartConfig = { export function ChartOpenedResolved() { const [timeRange, setTimeRange] = React.useState("30d") const [companyId, setCompanyId] = usePersistentCompanyFilter("all") - const { session, convexUserId } = useAuth() + const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID + const reportsEnabled = Boolean(isStaff && convexUserId) const data = useQuery( api.reports.openedResolvedByDay, - convexUserId + reportsEnabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, @@ -40,7 +41,10 @@ export function ChartOpenedResolved() { : "skip" ) as { rangeDays: number; series: SeriesPoint[] } | undefined - const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + const companies = useQuery( + api.companies.list, + reportsEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" + ) as Array<{ id: Id<"companies">; name: string }> | undefined if (!data) { return @@ -109,4 +113,3 @@ export function ChartOpenedResolved() { ) } - diff --git a/src/components/charts/views-charts.tsx b/src/components/charts/views-charts.tsx index c0a658a..156cf7e 100644 --- a/src/components/charts/views-charts.tsx +++ b/src/components/charts/views-charts.tsx @@ -26,15 +26,15 @@ export function ViewsCharts() { function BacklogPriorityPie() { const [companyId, setCompanyId] = usePersistentCompanyFilter("all") const [timeRange, setTimeRange] = React.useState("30d") - const { session, convexUserId } = useAuth() + const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const data = useQuery( api.reports.backlogOverview, - convexUserId + isStaff && convexUserId ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) as { priorityCounts: Record } | undefined - const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + const companies = useQuery(api.companies.list, isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined if (!data) return const PRIORITY_LABELS: Record = { LOW: "Baixa", MEDIUM: "Média", HIGH: "Alta", URGENT: "Crítica" } @@ -106,15 +106,15 @@ function BacklogPriorityPie() { function QueuesOpenBar() { const [companyId, setCompanyId] = usePersistentCompanyFilter("all") - const { session, convexUserId } = useAuth() + const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const data = useQuery( api.reports.slaOverview, - convexUserId + isStaff && convexUserId ? ({ tenantId, viewerId: convexUserId as Id<"users">, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) as { queueBreakdown: { id: string; name: string; open: number }[] } | undefined - const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + const companies = useQuery(api.companies.list, isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined if (!data) return const chartData = (data.queueBreakdown ?? []).map((q) => ({ queue: q.name, open: q.open })) diff --git a/src/components/section-cards.tsx b/src/components/section-cards.tsx index f8657ee..4d3b339 100644 --- a/src/components/section-cards.tsx +++ b/src/components/section-cards.tsx @@ -33,12 +33,13 @@ function formatScore(value: number | null) { } export function SectionCards() { - const { session, convexUserId } = useAuth() + const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID + const dashboardEnabled = Boolean(isStaff && convexUserId) const dashboard = useQuery( api.reports.dashboardOverview, - convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" + dashboardEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) const trendInfo = useMemo(() => { diff --git a/src/components/tickets/new-ticket-dialog.tsx b/src/components/tickets/new-ticket-dialog.tsx index d4c5516..2831853 100644 --- a/src/components/tickets/new-ticket-dialog.tsx +++ b/src/components/tickets/new-ticket-dialog.tsx @@ -56,14 +56,15 @@ export function NewTicketDialog() { }, mode: "onTouched", }) - const { convexUserId } = useAuth() - const queueArgs = convexUserId + const { convexUserId, isStaff } = useAuth() + const queuesEnabled = Boolean(isStaff && convexUserId) + const queueArgs = queuesEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip" useDefaultQueues(DEFAULT_TENANT_ID) const queuesRaw = useQuery( - convexUserId ? api.queues.summary : "skip", + queuesEnabled ? api.queues.summary : "skip", queueArgs ) as TicketQueueSummary[] | undefined const queues = useMemo(() => queuesRaw ?? [], [queuesRaw]) diff --git a/src/components/tickets/play-next-ticket-card.tsx b/src/components/tickets/play-next-ticket-card.tsx index 0199811..7bdeed3 100644 --- a/src/components/tickets/play-next-ticket-card.tsx +++ b/src/components/tickets/play-next-ticket-card.tsx @@ -30,12 +30,13 @@ const secondaryButtonClass = "inline-flex items-center gap-2 rounded-lg border b export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) { const router = useRouter() - const { convexUserId } = useAuth() - const queueArgs = convexUserId + const { convexUserId, isStaff } = useAuth() + const queuesEnabled = Boolean(isStaff && convexUserId) + const queueArgs = queuesEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip" const queueSummary = ( - useQuery(convexUserId ? api.queues.summary : "skip", queueArgs) as TicketQueueSummary[] | undefined + useQuery(queuesEnabled ? api.queues.summary : "skip", queueArgs) as TicketQueueSummary[] | undefined ) ?? [] const playNext = useMutation(api.tickets.playNext) const [selectedQueueId, setSelectedQueueId] = useState(undefined) diff --git a/src/components/tickets/ticket-queue-summary.tsx b/src/components/tickets/ticket-queue-summary.tsx index 659fccb..5abe568 100644 --- a/src/components/tickets/ticket-queue-summary.tsx +++ b/src/components/tickets/ticket-queue-summary.tsx @@ -14,11 +14,12 @@ interface TicketQueueSummaryProps { } export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) { - const { convexUserId } = useAuth() - const queueArgs = convexUserId + const { convexUserId, isStaff } = useAuth() + const enabled = Boolean(isStaff && convexUserId) + const queueArgs = enabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip" - const fromServer = useQuery(convexUserId ? api.queues.summary : "skip", queueArgs) + const fromServer = useQuery(enabled ? api.queues.summary : "skip", queueArgs) const data: TicketQueueSummary[] = (queues ?? (fromServer as TicketQueueSummary[] | undefined) ?? []) if (!queues && fromServer === undefined) { diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index da1be72..8bf339f 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -88,7 +88,7 @@ function formatDuration(durationMs: number) { } export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { - const { convexUserId, role } = useAuth() + const { convexUserId, role, isStaff } = useAuth() const isManager = role === "manager" useDefaultQueues(ticket.tenantId) const changeAssignee = useMutation(api.tickets.changeAssignee) @@ -99,11 +99,12 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { const pauseWork = useMutation(api.tickets.pauseWork) const updateCategories = useMutation(api.tickets.updateCategories) const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? [] - const queueArgs = convexUserId + const queuesEnabled = Boolean(isStaff && convexUserId) + const queueArgs = queuesEnabled ? { tenantId: ticket.tenantId, viewerId: convexUserId as Id<"users"> } : "skip" const queues = ( - useQuery(convexUserId ? api.queues.summary : "skip", queueArgs) as TicketQueueSummary[] | undefined + useQuery(queuesEnabled ? api.queues.summary : "skip", queueArgs) as TicketQueueSummary[] | undefined ) ?? [] const { categories, isLoading: categoriesLoading } = useTicketCategories(ticket.tenantId) const [status] = useState(ticket.status) diff --git a/src/components/tickets/tickets-view.tsx b/src/components/tickets/tickets-view.tsx index 702416e..84126bd 100644 --- a/src/components/tickets/tickets-view.tsx +++ b/src/components/tickets/tickets-view.tsx @@ -14,14 +14,15 @@ import { useDefaultQueues } from "@/hooks/use-default-queues" export function TicketsView() { const [filters, setFilters] = useState(defaultTicketFilters) - const { session, convexUserId } = useAuth() + const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID useDefaultQueues(tenantId) + const queuesEnabled = Boolean(isStaff && convexUserId) const queues = useQuery( - convexUserId ? api.queues.summary : "skip", - convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" + queuesEnabled ? api.queues.summary : "skip", + queuesEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as TicketQueueSummary[] | undefined const ticketsRaw = useQuery( api.tickets.list,