refactor: Update email configuration page to load data once and improve error handling, and add Svelte agent rules documentation.

This commit is contained in:
2025-11-22 10:25:43 -03:00
parent ce94eb53b3
commit b8a2e67f3a
2 changed files with 530 additions and 532 deletions

View File

@@ -0,0 +1,28 @@
---
trigger: glob
globs: **/*.svelte.ts,**/*.svelte
---
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.

View File

@@ -1,51 +1,45 @@
<script lang="ts"> <script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte"; import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import type { FunctionReference } from 'convex/server';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
const configAtual = useQuery(api.configuracaoEmail.obterConfigEmail, {}); const configAtual = useQuery(api.configuracaoEmail.obterConfigEmail);
let servidor = $state(""); let servidor = $state('');
let porta = $state(587); let porta = $state(587);
let usuario = $state(""); let usuario = $state('');
let senha = $state(""); let senha = $state('');
let emailRemetente = $state(""); let emailRemetente = $state('');
let nomeRemetente = $state(""); let nomeRemetente = $state('');
let usarSSL = $state(false); let usarSSL = $state(false);
let usarTLS = $state(true); let usarTLS = $state(true);
let processando = $state(false); let processando = $state(false);
let testando = $state(false); let testando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>( let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
null,
);
function mostrarMensagem(tipo: "success" | "error", texto: string) { function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
mensagem = { tipo, texto }; mensagem = { tipo, texto };
setTimeout(() => { setTimeout(() => {
mensagem = null; mensagem = null;
}, 5000); }, 5000);
} }
// Carregar config existente let dataLoaded = $state(false);
// Carregar config existente apenas uma vez
$effect(() => { $effect(() => {
if (configAtual?.data) { if (configAtual?.data && !dataLoaded) {
servidor = configAtual.data.servidor || ""; servidor = configAtual.data.servidor || '';
porta = configAtual.data.porta || 587; porta = configAtual.data.porta || 587;
usuario = configAtual.data.usuario || ""; usuario = configAtual.data.usuario || '';
emailRemetente = configAtual.data.emailRemetente || ""; emailRemetente = configAtual.data.emailRemetente || '';
nomeRemetente = configAtual.data.nomeRemetente || ""; nomeRemetente = configAtual.data.nomeRemetente || '';
usarSSL = configAtual.data.usarSSL || false; usarSSL = configAtual.data.usarSSL || false;
usarTLS = configAtual.data.usarTLS || true; usarTLS = configAtual.data.usarTLS || true;
} dataLoaded = true;
});
// Tornar SSL e TLS mutuamente exclusivos
$effect(() => {
if (usarSSL && usarTLS) {
// Se ambos estão marcados, priorizar TLS por padrão
usarSSL = false;
} }
}); });
@@ -72,62 +66,61 @@
!emailRemetente?.trim() || !emailRemetente?.trim() ||
!nomeRemetente?.trim() !nomeRemetente?.trim()
) { ) {
mostrarMensagem("error", "Preencha todos os campos obrigatórios"); mostrarMensagem('error', 'Preencha todos os campos obrigatórios');
return; return;
} }
// Validação de porta (1-65535) // Validação de porta (1-65535)
const portaNum = Number(porta); const portaNum = Number(porta);
if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) { if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) {
mostrarMensagem("error", "Porta deve ser um número entre 1 e 65535"); mostrarMensagem('error', 'Porta deve ser um número entre 1 e 65535');
return; return;
} }
// Validação de formato de email // Validação de formato de email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailRemetente.trim())) { if (!emailRegex.test(emailRemetente.trim())) {
mostrarMensagem("error", "Email remetente inválido"); mostrarMensagem('error', 'Email remetente inválido');
return; return;
} }
// Validação de senha: obrigatória apenas se não houver configuração existente // Validação de senha: obrigatória apenas se não houver configuração existente
const temConfigExistente = configAtual?.data?.ativo; const temConfigExistente = configAtual?.data?.ativo;
if (!temConfigExistente && !senha) { if (!temConfigExistente && !senha) {
mostrarMensagem("error", "Senha é obrigatória para nova configuração"); mostrarMensagem('error', 'Senha é obrigatória para nova configuração');
return; return;
} }
if (!currentUser?.data) { if (!currentUser?.data) {
mostrarMensagem("error", "Usuário não autenticado"); mostrarMensagem('error', 'Usuário não autenticado');
return; return;
} }
processando = true; processando = true;
try { try {
const resultado = await client.mutation( const resultado = await client.mutation(api.configuracaoEmail.salvarConfigEmail, {
api.configuracaoEmail.salvarConfigEmail,
{
servidor: servidor.trim(), servidor: servidor.trim(),
porta: portaNum, porta: portaNum,
usuario: usuario.trim(), usuario: usuario.trim(),
senha: senha || "", // Senha vazia será tratada no backend senha: senha || '', // Senha vazia será tratada no backend
emailRemetente: emailRemetente.trim(), emailRemetente: emailRemetente.trim(),
nomeRemetente: nomeRemetente.trim(), nomeRemetente: nomeRemetente.trim(),
usarSSL, usarSSL,
usarTLS, usarTLS,
configuradoPorId: currentUser.data._id as Id<"usuarios">, configuradoPorId: currentUser.data._id as Id<'usuarios'>
}, });
);
if (resultado.sucesso) { if (resultado.sucesso) {
mostrarMensagem("success", "Configuração salva com sucesso!"); mostrarMensagem('success', 'Configuração salva com sucesso!');
senha = ""; // Limpar senha senha = ''; // Limpar senha
} else { } else {
mostrarMensagem("error", resultado.erro); mostrarMensagem('error', resultado.erro);
}
} catch (error) {
if (error instanceof Error) {
console.error('Erro ao salvar configuração:', error);
mostrarMensagem('error', error.message || 'Erro ao salvar configuração');
} }
} catch (error: any) {
console.error("Erro ao salvar configuração:", error);
mostrarMensagem("error", error.message || "Erro ao salvar configuração");
} finally { } finally {
processando = false; processando = false;
} }
@@ -135,66 +128,56 @@
async function testarConexao() { async function testarConexao() {
if (!servidor?.trim() || !porta || !usuario?.trim() || !senha) { if (!servidor?.trim() || !porta || !usuario?.trim() || !senha) {
mostrarMensagem("error", "Preencha os dados de conexão antes de testar"); mostrarMensagem('error', 'Preencha os dados de conexão antes de testar');
return; return;
} }
// Validação de porta // Validação de porta
const portaNum = Number(porta); const portaNum = Number(porta);
if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) { if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) {
mostrarMensagem("error", "Porta deve ser um número entre 1 e 65535"); mostrarMensagem('error', 'Porta deve ser um número entre 1 e 65535');
return; return;
} }
testando = true; testando = true;
try { try {
const resultado = await client.action( const resultado = await client.action(api.configuracaoEmail.testarConexaoSMTP, {
api.configuracaoEmail.testarConexaoSMTP,
{
servidor: servidor.trim(), servidor: servidor.trim(),
porta: portaNum, porta: portaNum,
usuario: usuario.trim(), usuario: usuario.trim(),
senha: senha, senha: senha,
usarSSL, usarSSL,
usarTLS, usarTLS
}, });
);
if (resultado.sucesso) { if (resultado.sucesso) {
mostrarMensagem( mostrarMensagem('success', 'Conexão testada com sucesso! Servidor SMTP está respondendo.');
"success",
"Conexão testada com sucesso! Servidor SMTP está respondendo.",
);
} else { } else {
mostrarMensagem("error", `Erro ao testar conexão: ${resultado.erro}`); mostrarMensagem('error', `Erro ao testar conexão: ${resultado.erro}`);
}
} catch (error) {
if (error instanceof Error) {
console.error('Erro ao testar conexão:', error);
mostrarMensagem('error', error.message || 'Erro ao conectar com o servidor SMTP');
} }
} catch (error: any) {
console.error("Erro ao testar conexão:", error);
mostrarMensagem(
"error",
error.message || "Erro ao conectar com o servidor SMTP",
);
} finally { } finally {
testando = false; testando = false;
} }
} }
const statusConfig = $derived( const statusConfig = $derived(configAtual?.data?.ativo ? 'Configurado' : 'Não configurado');
configAtual?.data?.ativo ? "Configurado" : "Não configurado",
);
const isLoading = $derived(configAtual === undefined); const isLoading = $derived(configAtual === undefined);
const hasError = $derived(configAtual === null && !isLoading);
</script> </script>
<div class="container mx-auto px-4 py-6 max-w-4xl"> <div class="container mx-auto max-w-4xl px-4 py-6">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between mb-6"> <div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="p-3 bg-secondary/10 rounded-xl"> <div class="bg-secondary/10 rounded-xl p-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-secondary" class="text-secondary h-8 w-8"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -208,9 +191,7 @@
</svg> </svg>
</div> </div>
<div> <div>
<h1 class="text-3xl font-bold text-base-content"> <h1 class="text-base-content text-3xl font-bold">Configurações de Email (SMTP)</h1>
Configurações de Email (SMTP)
</h1>
<p class="text-base-content/60 mt-1"> <p class="text-base-content/60 mt-1">
Configurar servidor de email para envio de notificações Configurar servidor de email para envio de notificações
</p> </p>
@@ -222,16 +203,16 @@
{#if mensagem} {#if mensagem}
<div <div
class="alert mb-6" class="alert mb-6"
class:alert-success={mensagem.tipo === "success"} class:alert-success={mensagem.tipo === 'success'}
class:alert-error={mensagem.tipo === "error"} class:alert-error={mensagem.tipo === 'error'}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
{#if mensagem.tipo === "success"} {#if mensagem.tipo === 'success'}
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@@ -261,16 +242,12 @@
<!-- Status --> <!-- Status -->
{#if !isLoading} {#if !isLoading}
<div <div class="alert {configAtual?.data?.ativo ? 'alert-success' : 'alert-warning'} mb-6">
class="alert {configAtual?.data?.ativo
? 'alert-success'
: 'alert-warning'} mb-6"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6" class="h-6 w-6 shrink-0 stroke-current"
> >
{#if configAtual?.data?.ativo} {#if configAtual?.data?.ativo}
<path <path
@@ -292,9 +269,7 @@
<strong>Status:</strong> <strong>Status:</strong>
{statusConfig} {statusConfig}
{#if configAtual?.data?.testadoEm} {#if configAtual?.data?.testadoEm}
- Última conexão testada em {new Date( - Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')}
configAtual.data.testadoEm,
).toLocaleString("pt-BR")}
{/if} {/if}
</span> </span>
</div> </div>
@@ -306,7 +281,7 @@
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-4">Dados do Servidor SMTP</h2> <h2 class="card-title mb-4">Dados do Servidor SMTP</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Servidor --> <!-- Servidor -->
<div class="form-control md:col-span-1"> <div class="form-control md:col-span-1">
<label class="label" for="smtp-servidor"> <label class="label" for="smtp-servidor">
@@ -320,9 +295,7 @@
class="input input-bordered" class="input input-bordered"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt" <span class="label-text-alt">Ex: smtp.gmail.com, smtp.office365.com</span>
>Ex: smtp.gmail.com, smtp.office365.com</span
>
</div> </div>
</div> </div>
@@ -339,8 +312,7 @@
class="input input-bordered" class="input input-bordered"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt">Comum: 587 (TLS), 465 (SSL), 25</span <span class="label-text-alt">Comum: 587 (TLS), 465 (SSL), 25</span>
>
</div> </div>
</div> </div>
@@ -412,7 +384,7 @@
<!-- Opções de Segurança --> <!-- Opções de Segurança -->
<div class="divider"></div> <div class="divider"></div>
<h3 class="font-bold mb-2">Configurações de Segurança</h3> <h3 class="mb-2 font-bold">Configurações de Segurança</h3>
<div class="flex flex-wrap gap-6"> <div class="flex flex-wrap gap-6">
<div class="form-control"> <div class="form-control">
@@ -441,7 +413,7 @@
</div> </div>
<!-- Ações --> <!-- Ações -->
<div class="card-actions justify-end mt-6 gap-3"> <div class="card-actions mt-6 justify-end gap-3">
<button <button
class="btn btn-outline btn-info" class="btn btn-outline btn-info"
onclick={testarConexao} onclick={testarConexao}
@@ -499,12 +471,12 @@
{/if} {/if}
<!-- Exemplos Comuns --> <!-- Exemplos Comuns -->
<div class="card bg-base-100 shadow-xl mt-6"> <div class="card bg-base-100 mt-6 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mb-4">Exemplos de Configuração</h2> <h2 class="card-title mb-4">Exemplos de Configuração</h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-sm"> <table class="table-sm table">
<thead> <thead>
<tr> <tr>
<th>Provedor</th> <th>Provedor</th>
@@ -550,7 +522,7 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6" class="h-6 w-6 shrink-0 stroke-current"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@@ -561,13 +533,11 @@
</svg> </svg>
<div> <div>
<p> <p>
<strong>Dica de Segurança:</strong> Para Gmail e outros provedores, você <strong>Dica de Segurança:</strong> Para Gmail e outros provedores, você pode precisar gerar
pode precisar gerar uma "senha de app" específica em vez de usar sua senha uma "senha de app" específica em vez de usar sua senha principal.
principal.
</p> </p>
<p class="text-sm mt-1"> <p class="mt-1 text-sm">
Gmail: Conta Google → Segurança → Verificação em duas etapas → Senhas de Gmail: Conta Google → Segurança → Verificação em duas etapas → Senhas de app
app
</p> </p>
</div> </div>
</div> </div>