feat: integrate Jitsi configuration and dynamic loading in CallWindow

- Added support for Jitsi configuration retrieval from the backend, allowing for dynamic room name generation based on the active configuration.
- Implemented a polyfill for BlobBuilder to ensure compatibility with the lib-jitsi-meet library across different browsers.
- Enhanced error handling during the loading of the Jitsi library, providing clearer feedback for missing modules and connection issues.
- Updated Vite configuration to exclude lib-jitsi-meet from SSR and allow dynamic loading in the browser.
- Introduced a new route for Jitsi settings in the dashboard for user configuration of Jitsi Meet parameters.
This commit is contained in:
2025-11-21 22:03:01 -03:00
parent 41f7942dd1
commit 52823a9fac
9 changed files with 1100 additions and 23 deletions

View File

@@ -12,7 +12,8 @@
| 'document'
| 'teams'
| 'userPlus'
| 'clock';
| 'clock'
| 'video';
type PaletteKey = 'primary' | 'success' | 'secondary' | 'accent' | 'info' | 'error' | 'warning';
type TiRouteId =
@@ -28,7 +29,8 @@
| '/(dashboard)/ti/notificacoes'
| '/(dashboard)/ti/monitoramento'
| '/(dashboard)/ti/configuracoes-ponto'
| '/(dashboard)/ti/configuracoes-relogio';
| '/(dashboard)/ti/configuracoes-relogio'
| '/(dashboard)/ti/configuracoes-jitsi';
type FeatureCard = {
title: string;
@@ -202,6 +204,13 @@
strokeLinecap: 'round',
strokeLinejoin: 'round'
}
],
video: [
{
d: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z',
strokeLinecap: 'round',
strokeLinejoin: 'round'
}
]
};
@@ -259,6 +268,15 @@
palette: 'secondary',
icon: 'envelope'
},
{
title: 'Configurações do Jitsi',
description:
'Configure o servidor Jitsi Meet para chamadas de vídeo e áudio no chat. Ajuste domínio, App ID e prefixo de salas.',
ctaLabel: 'Configurar Jitsi',
href: '/(dashboard)/ti/configuracoes-jitsi',
palette: 'primary',
icon: 'video'
},
{
title: 'Configurações de Ponto',
description:

View File

@@ -0,0 +1,532 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
const configAtual = useQuery(api.configuracaoJitsi.obterConfigJitsi, {});
let domain = $state("");
let appId = $state("sgse-app");
let roomPrefix = $state("sgse");
let useHttps = $state(false);
let acceptSelfSignedCert = $state(false);
let processando = $state(false);
let testando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
function mostrarMensagem(tipo: "success" | "error", texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 5000);
}
// Carregar config existente
$effect(() => {
if (configAtual?.data) {
domain = configAtual.data.domain || "";
appId = configAtual.data.appId || "sgse-app";
roomPrefix = configAtual.data.roomPrefix || "sgse";
useHttps = configAtual.data.useHttps || false;
acceptSelfSignedCert = configAtual.data.acceptSelfSignedCert || false;
}
});
// Ativar HTTPS automaticamente se domínio contém porta 8443
$effect(() => {
if (domain.includes(":8443")) {
useHttps = true;
// Para localhost com porta 8443, geralmente é certificado autoassinado
if (domain.includes("localhost")) {
acceptSelfSignedCert = true;
}
}
});
async function salvarConfiguracao() {
// Validação de campos obrigatórios
if (!domain?.trim() || !appId?.trim() || !roomPrefix?.trim()) {
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
return;
}
// Validação de roomPrefix (apenas letras, números e hífens)
const roomPrefixRegex = /^[a-zA-Z0-9-]+$/;
if (!roomPrefixRegex.test(roomPrefix.trim())) {
mostrarMensagem(
"error",
"Prefixo de sala deve conter apenas letras, números e hífens"
);
return;
}
if (!currentUser?.data) {
mostrarMensagem("error", "Usuário não autenticado");
return;
}
processando = true;
try {
const resultado = await client.mutation(api.configuracaoJitsi.salvarConfigJitsi, {
domain: domain.trim(),
appId: appId.trim(),
roomPrefix: roomPrefix.trim(),
useHttps,
acceptSelfSignedCert,
configuradoPorId: currentUser.data._id as Id<"usuarios">,
});
if (resultado.sucesso) {
mostrarMensagem("success", "Configuração salva com sucesso!");
} else {
mostrarMensagem("error", resultado.erro);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Erro ao salvar configuração:", error);
mostrarMensagem("error", errorMessage || "Erro ao salvar configuração");
} finally {
processando = false;
}
}
async function testarConexao() {
if (!domain?.trim()) {
mostrarMensagem("error", "Preencha o domínio antes de testar");
return;
}
testando = true;
try {
const resultado = await client.action(api.configuracaoJitsi.testarConexaoJitsi, {
domain: domain.trim(),
useHttps,
acceptSelfSignedCert,
});
if (resultado.sucesso) {
const mensagemSucesso = resultado.aviso
? `Conexão testada com sucesso! ${resultado.aviso}`
: "Conexão testada com sucesso! Servidor Jitsi está acessível.";
mostrarMensagem("success", mensagemSucesso);
} else {
mostrarMensagem("error", `Erro ao testar conexão: ${resultado.erro}`);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Erro ao testar conexão:", error);
mostrarMensagem(
"error",
errorMessage || "Erro ao conectar com o servidor Jitsi"
);
} finally {
testando = false;
}
}
const statusConfig = $derived(
configAtual?.data?.ativo ? "Configurado" : "Não configurado"
);
const isLoading = $derived(configAtual === undefined);
const hasError = $derived(configAtual === null && !isLoading);
</script>
<div class="container mx-auto px-4 py-6 max-w-4xl">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Configurações do Jitsi Meet</h1>
<p class="text-base-content/60 mt-1">
Configurar servidor Jitsi para chamadas de vídeo e áudio
</p>
</div>
</div>
</div>
<!-- Mensagens -->
{#if mensagem}
<div
class="alert mb-6"
class:alert-success={mensagem.tipo === "success"}
class:alert-error={mensagem.tipo === "error"}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
{#if mensagem.tipo === "success"}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
{/if}
</svg>
<span>{mensagem.texto}</span>
</div>
{/if}
<!-- Loading State -->
{#if isLoading}
<div class="alert alert-info mb-6">
<span class="loading loading-spinner loading-sm"></span>
<span>Carregando configurações...</span>
</div>
{/if}
<!-- Status -->
{#if !isLoading}
<div
class="alert {configAtual?.data?.ativo
? 'alert-success'
: 'alert-warning'} mb-6"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
{#if configAtual?.data?.ativo}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
{/if}
</svg>
<span>
<strong>Status:</strong>
{statusConfig}
{#if configAtual?.data?.testadoEm}
- Última conexão testada em {new Date(
configAtual.data.testadoEm
).toLocaleString("pt-BR")}
{/if}
</span>
</div>
{/if}
<!-- Formulário -->
{#if !isLoading}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Dados do Servidor Jitsi</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Domínio -->
<div class="form-control md:col-span-2">
<label class="label" for="jitsi-domain">
<span class="label-text font-medium">Domínio do Servidor *</span>
</label>
<input
id="jitsi-domain"
type="text"
bind:value={domain}
placeholder="localhost:8443 ou meet.example.com"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt"
>Ex: localhost:8443 (local), meet.example.com (produção)</span
>
</div>
</div>
<!-- App ID -->
<div class="form-control">
<label class="label" for="jitsi-app-id">
<span class="label-text font-medium">App ID *</span>
</label>
<input
id="jitsi-app-id"
type="text"
bind:value={appId}
placeholder="sgse-app"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Identificador da aplicação Jitsi</span>
</div>
</div>
<!-- Room Prefix -->
<div class="form-control">
<label class="label" for="jitsi-room-prefix">
<span class="label-text font-medium">Prefixo de Sala *</span>
</label>
<input
id="jitsi-room-prefix"
type="text"
bind:value={roomPrefix}
placeholder="sgse"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt"
>Apenas letras, números e hífens</span
>
</div>
</div>
</div>
<!-- Opções de Segurança -->
<div class="divider"></div>
<h3 class="font-bold mb-2">Configurações de Segurança</h3>
<div class="flex flex-col gap-4">
<div class="form-control">
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
bind:checked={useHttps}
class="checkbox checkbox-primary"
/>
<span class="label-text font-medium">Usar HTTPS</span>
</label>
<div class="label">
<span class="label-text-alt"
>Ativado automaticamente se domínio contém :8443. Desmarque para usar HTTP (não recomendado para produção)</span
>
</div>
</div>
<div class="form-control">
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
bind:checked={acceptSelfSignedCert}
class="checkbox checkbox-warning"
/>
<span class="label-text font-medium">Aceitar Certificados Autoassinados</span>
</label>
<div class="label">
<span class="label-text-alt text-warning"
>Habilitar apenas para desenvolvimento local com certificados autoassinados. Em produção, use certificados válidos.</span
>
</div>
</div>
</div>
<!-- Ações -->
<div class="card-actions justify-end mt-6 gap-3">
<button
class="btn btn-outline btn-info"
onclick={testarConexao}
disabled={testando || processando}
>
{#if testando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
Testar Conexão
</button>
<button
class="btn btn-primary"
onclick={salvarConfiguracao}
disabled={processando || testando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
/>
</svg>
{/if}
Salvar Configuração
</button>
</div>
</div>
</div>
{/if}
<!-- Exemplos Comuns -->
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card-body">
<h2 class="card-title mb-4">Exemplos de Configuração</h2>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Ambiente</th>
<th>Domínio</th>
<th>App ID</th>
<th>Prefixo Sala</th>
<th>HTTPS</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Docker Local</strong></td>
<td>localhost:8443</td>
<td>sgse-app</td>
<td>sgse</td>
<td>Sim</td>
</tr>
<tr>
<td><strong>Produção</strong></td>
<td>meet.example.com</td>
<td>sgse-app</td>
<td>sgse</td>
<td>Sim</td>
</tr>
<tr>
<td><strong>Desenvolvimento</strong></td>
<td>localhost:8000</td>
<td>sgse-app</td>
<td>sgse-dev</td>
<td>Não</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Avisos -->
<div class="alert alert-info mt-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>
<p>
<strong>Dica:</strong> Para servidor Jitsi Docker local, use
<code>localhost:8443</code> com HTTPS habilitado. Para servidor em
produção, use o domínio completo do seu servidor Jitsi.
</p>
<p class="text-sm mt-1">
A configuração será aplicada imediatamente após salvar. Usuários precisarão
recarregar a página para usar a nova configuração.
</p>
</div>
</div>
<!-- Aviso sobre Certificados Autoassinados -->
{#if acceptSelfSignedCert}
<div class="alert alert-warning mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
></path>
</svg>
<div>
<p class="font-bold">Certificados Autoassinados Ativados</p>
<p class="text-sm mt-1">
Para certificados autoassinados (desenvolvimento local), os usuários precisarão
aceitar o certificado no navegador na primeira conexão. Em produção, use
certificados válidos (Let's Encrypt, etc.).
</p>
</div>
</div>
{/if}
<!-- Aviso sobre HTTP -->
{#if !useHttps}
<div class="alert alert-error mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
></path>
</svg>
<div>
<p class="font-bold">HTTP Ativado (Não Seguro)</p>
<p class="text-sm mt-1">
O uso de HTTP não é recomendado para produção. Use HTTPS com certificado válido
para garantir segurança nas chamadas.
</p>
</div>
</div>
{/if}
</div>