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,