Compare commits
12 Commits
feat-fluxo
...
config-sel
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e7de6c943 | |||
| af21a35f05 | |||
| 277dc616b3 | |||
| 0c0c7a29c0 | |||
| be959eb230 | |||
| 86ae2a1084 | |||
| e1bd6fa61a | |||
| 75989b0546 | |||
| 08869fe5da | |||
| 71959f6553 | |||
| de694ed665 | |||
| daee99191c |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
37
.github/workflows/deploy.yml
vendored
Normal file
37
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Build Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
|
||||
jobs:
|
||||
build-and-push-dockerfile-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }} # Make sure to add the secrets in your repository in -> Settings -> Secrets (Actions) -> New repository secret
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }} # Make sure to add the secrets in your repository in -> Settings -> Secrets (Actions) -> New repository secret
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/web/Dockerfile
|
||||
push: true
|
||||
# Make sure to replace with your own namespace and repository
|
||||
tags: |
|
||||
killercf/sgc:latest
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
PUBLIC_CONVEX_URL=${{ secrets.PUBLIC_CONVEX_URL }}
|
||||
PUBLIC_CONVEX_SITE_URL=${{ secrets.PUBLIC_CONVEX_SITE_URL }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,3 +49,5 @@ coverage
|
||||
tmp
|
||||
temp
|
||||
.eslintcache
|
||||
|
||||
out
|
||||
@@ -1,296 +0,0 @@
|
||||
# Correções Implementadas para Integração Jitsi
|
||||
|
||||
## Resumo das Alterações
|
||||
|
||||
Este documento descreve todas as correções implementadas para integrar o servidor Jitsi ao projeto SGSE e fazer as chamadas de áudio e vídeo funcionarem corretamente.
|
||||
|
||||
---
|
||||
|
||||
## 1. Configuração do JitsiConnection
|
||||
|
||||
### Problema Identificado
|
||||
- A configuração do `serviceUrl` e `muc` estava incorreta para Docker Jitsi local
|
||||
- O domínio incluía a porta, causando problemas na conexão
|
||||
|
||||
### Correção Implementada
|
||||
```typescript
|
||||
// Separar host e porta corretamente
|
||||
const { host, porta } = obterHostEPorta(config.domain);
|
||||
const protocol = config.useHttps ? 'https' : 'http';
|
||||
|
||||
const options = {
|
||||
hosts: {
|
||||
domain: host, // Apenas o host (sem porta)
|
||||
muc: `conference.${host}` // MUC no mesmo domínio
|
||||
},
|
||||
serviceUrl: `${protocol}://${host}:${porta}/http-bind`, // BOSH com porta
|
||||
bosh: `${protocol}://${host}:${porta}/http-bind`, // BOSH alternativo
|
||||
clientNode: config.appId
|
||||
};
|
||||
```
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
**Arquivo criado/atualizado:**
|
||||
- `apps/web/src/lib/utils/jitsi.ts` - Adicionada função `obterHostEPorta()`
|
||||
|
||||
---
|
||||
|
||||
## 2. Criação de Tracks Locais
|
||||
|
||||
### Problema Identificado
|
||||
- Os tracks locais não estavam sendo criados após entrar na conferência
|
||||
- Faltava o evento `CONFERENCE_JOINED` para criar tracks locais
|
||||
|
||||
### Correção Implementada
|
||||
```typescript
|
||||
conference.on(JitsiMeetJS.constants.events.conference.CONFERENCE_JOINED, async () => {
|
||||
// Criar tracks locais com constraints apropriadas
|
||||
const constraints = {
|
||||
audio: estadoAtual.audioHabilitado ? {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
} : false,
|
||||
video: estadoAtual.videoHabilitado ? {
|
||||
facingMode: 'user',
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
} : false
|
||||
};
|
||||
|
||||
const tracks = await JitsiMeetJS.createLocalTracks(constraints, {
|
||||
devices: [],
|
||||
cameraDeviceId: estadoChamada.dispositivos.cameraId || undefined,
|
||||
micDeviceId: estadoChamada.dispositivos.microphoneId || undefined
|
||||
});
|
||||
|
||||
// Adicionar tracks à conferência e anexar ao vídeo local
|
||||
for (const track of tracks) {
|
||||
await conference.addTrack(track);
|
||||
if (track.getType() === 'video' && localVideo) {
|
||||
track.attach(localVideo);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 3. Gerenciamento de Tracks
|
||||
|
||||
### Problema Identificado
|
||||
- Tracks locais não eram armazenados corretamente
|
||||
- Falta de limpeza adequada ao finalizar chamada
|
||||
|
||||
### Correção Implementada
|
||||
- Adicionada variável de estado `localTracks: JitsiTrack[]` para rastrear todos os tracks locais
|
||||
- Implementada limpeza adequada no método `finalizar()`:
|
||||
- Desconectar tracks antes de liberar
|
||||
- Dispor de todos os tracks locais
|
||||
- Limpar referências
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 4. Attach/Detach de Tracks Remotos
|
||||
|
||||
### Problema Identificado
|
||||
- Tracks remotos não eram anexados corretamente aos elementos de vídeo/áudio
|
||||
- Não havia tratamento específico para áudio vs vídeo
|
||||
|
||||
### Correção Implementada
|
||||
```typescript
|
||||
function adicionarTrackRemoto(track: JitsiTrack): void {
|
||||
const participantId = track.getParticipantId();
|
||||
const trackType = track.getType();
|
||||
|
||||
if (trackType === 'audio') {
|
||||
// Criar elemento de áudio invisível
|
||||
const audioElement = document.createElement('audio');
|
||||
audioElement.id = `remote-audio-${participantId}`;
|
||||
audioElement.autoplay = true;
|
||||
track.attach(audioElement);
|
||||
videoContainer.appendChild(audioElement);
|
||||
} else if (trackType === 'video') {
|
||||
// Criar elemento de vídeo
|
||||
const videoElement = document.createElement('video');
|
||||
videoElement.id = `remote-video-${participantId}`;
|
||||
videoElement.autoplay = true;
|
||||
track.attach(videoElement);
|
||||
videoContainer.appendChild(videoElement);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 5. Controles de Áudio e Vídeo
|
||||
|
||||
### Problema Identificado
|
||||
- Os métodos `handleToggleAudio` e `handleToggleVideo` não criavam novos tracks quando necessário
|
||||
- Não atualizavam corretamente o estado dos tracks locais
|
||||
|
||||
### Correção Implementada
|
||||
- Implementada lógica para criar tracks se não existirem
|
||||
- Atualização correta do estado dos tracks (mute/unmute)
|
||||
- Sincronização com o backend quando anfitrião
|
||||
- Anexar/desanexar tracks ao vídeo local corretamente
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 6. Tratamento de Erros
|
||||
|
||||
### Problema Identificado
|
||||
- Uso de `alert()` para erros (não amigável)
|
||||
- Falta de mensagens de erro claras
|
||||
|
||||
### Correção Implementada
|
||||
- Implementado sistema de tratamento de erros com `ErrorModal`
|
||||
- Integrado com `traduzirErro()` para mensagens amigáveis
|
||||
- Adicionado estado de erro no componente:
|
||||
```typescript
|
||||
let showErrorModal = $state(false);
|
||||
let errorTitle = $state('Erro na Chamada');
|
||||
let errorMessage = $state('');
|
||||
let errorDetails = $state<string | undefined>(undefined);
|
||||
```
|
||||
|
||||
**Arquivos modificados:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
- Integração com `apps/web/src/lib/utils/erroHelpers.ts`
|
||||
|
||||
---
|
||||
|
||||
## 7. Inicialização do Jitsi Meet JS
|
||||
|
||||
### Problema Identificado
|
||||
- Configuração básica do Jitsi pode estar incompleta
|
||||
- Nível de log muito restritivo
|
||||
|
||||
### Correção Implementada
|
||||
```typescript
|
||||
JitsiMeetJS.init({
|
||||
disableAudioLevels: false, // Habilitado para melhor qualidade
|
||||
disableSimulcast: false,
|
||||
enableWindowOnErrorHandler: true,
|
||||
enableRemb: true, // REMB para controle de bitrate
|
||||
enableTcc: true, // TCC para controle de congestionamento
|
||||
disableThirdPartyRequests: false
|
||||
});
|
||||
|
||||
JitsiMeetJS.setLogLevel(JitsiMeetJS.constants.logLevels.INFO); // Mais verboso para debug
|
||||
```
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 8. UI/UX Melhorias
|
||||
|
||||
### Implementado
|
||||
- Indicador de conexão durante estabelecimento da chamada
|
||||
- Mensagem de "Conectando..." enquanto não há conexão estabelecida
|
||||
- Tratamento visual adequado de estados de conexão
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 9. Eventos da Conferência
|
||||
|
||||
### Adicionado
|
||||
- `CONFERENCE_JOINED`: Criar tracks locais após entrar
|
||||
- `CONFERENCE_LEFT`: Limpar tracks ao sair
|
||||
- Melhor tratamento de `TRACK_ADDED` e `TRACK_REMOVED`
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 10. Correção de Interfaces TypeScript
|
||||
|
||||
### Adicionado
|
||||
- Método `addTrack()` na interface `JitsiConference`
|
||||
- Melhor tipagem de `JitsiTrack` com propriedade `track: MediaStreamTrack`
|
||||
|
||||
**Arquivo modificado:**
|
||||
- `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
---
|
||||
|
||||
## Configuração Necessária
|
||||
|
||||
### Variáveis de Ambiente (.env)
|
||||
```env
|
||||
# Jitsi Meet Configuration (Docker Local)
|
||||
VITE_JITSI_DOMAIN=localhost:8443
|
||||
VITE_JITSI_APP_ID=sgse-app
|
||||
VITE_JITSI_ROOM_PREFIX=sgse
|
||||
VITE_JITSI_USE_HTTPS=true
|
||||
```
|
||||
|
||||
**Nota:** Para Docker Jitsi local, geralmente usa-se HTTPS na porta 8443.
|
||||
|
||||
---
|
||||
|
||||
## Verificações Necessárias
|
||||
|
||||
### 1. Docker Jitsi Rodando
|
||||
```bash
|
||||
docker ps | grep jitsi
|
||||
```
|
||||
|
||||
### 2. Porta 8443 Acessível
|
||||
```bash
|
||||
curl -k https://localhost:8443
|
||||
```
|
||||
|
||||
### 3. Permissões do Navegador
|
||||
- Microfone deve estar permitido
|
||||
- Câmera deve estar permitida (para chamadas de vídeo)
|
||||
|
||||
### 4. Logs do Navegador
|
||||
- Abrir DevTools (F12)
|
||||
- Verificar Console para erros de conexão
|
||||
- Verificar Network para erros de rede
|
||||
|
||||
---
|
||||
|
||||
## Próximos Passos (Se Necessário)
|
||||
|
||||
1. **Testar conectividade** - Verificar se o servidor Jitsi responde corretamente
|
||||
2. **Ajustar configuração de rede** - Se houver problemas de firewall ou CORS
|
||||
3. **Configurar STUN/TURN** - Para conexões através de NAT (se necessário)
|
||||
4. **Otimizar qualidade** - Ajustar bitrates e resoluções conforme necessário
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Todas as correções foram implementadas**
|
||||
✅ **Código sem erros de lint**
|
||||
✅ **Tratamento de erros adequado**
|
||||
✅ **Interfaces TypeScript corretas**
|
||||
✅ **Gerenciamento de recursos adequado**
|
||||
|
||||
---
|
||||
|
||||
**Data:** $(date)
|
||||
**Versão:** 1.0.0
|
||||
|
||||
@@ -1,701 +0,0 @@
|
||||
# Plano de Implementação - Chamadas de Áudio e Vídeo com Jitsi Meet
|
||||
|
||||
## Opção Escolhida: Docker Local (Desenvolvimento)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Etapas Fora do Código - Configuração Docker
|
||||
|
||||
### Etapa 1: Preparar Ambiente Docker
|
||||
|
||||
**Requisitos:**
|
||||
|
||||
- Docker Desktop instalado e rodando
|
||||
- Mínimo 4GB RAM disponível
|
||||
- Portas livres: 8000, 8443, 10000-20000/udp
|
||||
|
||||
**Passos:**
|
||||
|
||||
1. **Criar diretório para configuração Docker Jitsi:**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/jitsi-docker
|
||||
cd ~/jitsi-docker
|
||||
```
|
||||
|
||||
2. **Clonar repositório oficial:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jitsi/docker-jitsi-meet.git
|
||||
cd docker-jitsi-meet
|
||||
```
|
||||
|
||||
3. **Configurar variáveis de ambiente:**
|
||||
|
||||
```bash
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
4. **Editar arquivo `.env` com as seguintes configurações:**
|
||||
|
||||
```env
|
||||
# Configuração básica para desenvolvimento local
|
||||
CONFIG=~/.jitsi-meet-cfg
|
||||
TZ=America/Recife
|
||||
|
||||
# Desabilitar Let's Encrypt (não necessário para localhost)
|
||||
ENABLE_LETSENCRYPT=0
|
||||
|
||||
# Portas HTTP/HTTPS
|
||||
HTTP_PORT=8000
|
||||
HTTPS_PORT=8443
|
||||
|
||||
# Domínio local
|
||||
PUBLIC_URL=http://localhost:8000
|
||||
DOMAIN=localhost
|
||||
|
||||
# Desabilitar autenticação para facilitar testes
|
||||
ENABLE_AUTH=0
|
||||
ENABLE_GUESTS=1
|
||||
|
||||
# Desabilitar transcrissão (não necessário para desenvolvimento)
|
||||
ENABLE_TRANSCRIPTION=0
|
||||
|
||||
# Desabilitar gravação no servidor (usaremos gravação local)
|
||||
ENABLE_RECORDING=0
|
||||
|
||||
# Configurações de vídeo (ajustar conforme necessidade)
|
||||
ENABLE_PREJOIN_PAGE=0
|
||||
START_AUDIO_MUTED=0
|
||||
START_VIDEO_MUTED=0
|
||||
|
||||
# Configurações de segurança
|
||||
ENABLE_XMPP_WEBSOCKET=0
|
||||
ENABLE_P2P=1
|
||||
|
||||
# Limites
|
||||
MAX_NUMBER_OF_PARTICIPANTS=10
|
||||
RESOLUTION_WIDTH=1280
|
||||
RESOLUTION_HEIGHT=720
|
||||
```
|
||||
|
||||
5. **Criar diretórios necessários:**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.jitsi-meet-cfg/{web/letsencrypt,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb}
|
||||
```
|
||||
|
||||
6. **Iniciar containers:**
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
7. **Verificar status:**
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
8. **Ver logs se necessário:**
|
||||
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
9. **Testar acesso:**
|
||||
|
||||
- Acessar: http://localhost:8000
|
||||
- Criar uma sala de teste e verificar se funciona
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
- Se houver erro de permissão nos diretórios: `sudo chown -R $USER:$USER ~/.jitsi-meet-cfg`
|
||||
- Se portas estiverem em uso, alterar HTTP_PORT e HTTPS_PORT no .env
|
||||
- Para parar: `docker-compose down`
|
||||
- Para reiniciar: `docker-compose restart`
|
||||
|
||||
---
|
||||
|
||||
## 📦 Etapas no Código - Backend Convex
|
||||
|
||||
### Etapa 2: Atualizar Schema
|
||||
|
||||
**Arquivo:** `packages/backend/convex/schema.ts`
|
||||
|
||||
**Adicionar nova tabela `chamadas`:**
|
||||
|
||||
```typescript
|
||||
chamadas: defineTable({
|
||||
conversaId: v.id('conversas'),
|
||||
tipo: v.union(v.literal('audio'), v.literal('video')),
|
||||
roomName: v.string(), // Nome único da sala Jitsi
|
||||
criadoPor: v.id('usuarios'), // Anfitrião/criador
|
||||
participantes: v.array(v.id('usuarios')),
|
||||
status: v.union(
|
||||
v.literal('aguardando'),
|
||||
v.literal('em_andamento'),
|
||||
v.literal('finalizada'),
|
||||
v.literal('cancelada')
|
||||
),
|
||||
iniciadaEm: v.optional(v.number()),
|
||||
finalizadaEm: v.optional(v.number()),
|
||||
duracaoSegundos: v.optional(v.number()),
|
||||
gravando: v.boolean(),
|
||||
gravacaoIniciadaPor: v.optional(v.id('usuarios')),
|
||||
gravacaoIniciadaEm: v.optional(v.number()),
|
||||
gravacaoFinalizadaEm: v.optional(v.number()),
|
||||
configuracoes: v.optional(
|
||||
v.object({
|
||||
audioHabilitado: v.boolean(),
|
||||
videoHabilitado: v.boolean(),
|
||||
participantesConfig: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
usuarioId: v.id('usuarios'),
|
||||
audioHabilitado: v.boolean(),
|
||||
videoHabilitado: v.boolean(),
|
||||
forcadoPeloAnfitriao: v.optional(v.boolean()) // Se foi forçado pelo anfitrião
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
),
|
||||
criadoEm: v.number()
|
||||
})
|
||||
.index('by_conversa', ['conversaId', 'status'])
|
||||
.index('by_conversa_ativa', ['conversaId', 'status'])
|
||||
.index('by_criado_por', ['criadoPor'])
|
||||
.index('by_status', ['status'])
|
||||
.index('by_room_name', ['roomName']);
|
||||
```
|
||||
|
||||
### Etapa 3: Criar Backend de Chamadas
|
||||
|
||||
**Arquivo:** `packages/backend/convex/chamadas.ts`
|
||||
|
||||
**Funções a implementar:**
|
||||
|
||||
#### Mutations:
|
||||
|
||||
1. `criarChamada` - Criar nova chamada
|
||||
2. `iniciarChamada` - Marcar como em andamento
|
||||
3. `finalizarChamada` - Finalizar e calcular duração
|
||||
4. `adicionarParticipante` - Adicionar participante
|
||||
5. `removerParticipante` - Remover participante
|
||||
6. `toggleAudioVideo` - Anfitrião controla áudio/vídeo de participante
|
||||
7. `atualizarConfiguracaoParticipante` - Atualizar configuração individual
|
||||
8. `iniciarGravacao` - Marcar início de gravação
|
||||
9. `finalizarGravacao` - Marcar fim de gravação
|
||||
|
||||
#### Queries:
|
||||
|
||||
1. `obterChamadaAtiva` - Buscar chamada ativa de uma conversa
|
||||
2. `listarChamadas` - Listar histórico
|
||||
3. `verificarAnfitriao` - Verificar se usuário é anfitrião
|
||||
4. `obterParticipantesChamada` - Listar participantes
|
||||
|
||||
**Tipos TypeScript (sem usar `any`):**
|
||||
|
||||
```typescript
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
|
||||
type ChamadaTipo = 'audio' | 'video';
|
||||
type ChamadaStatus = 'aguardando' | 'em_andamento' | 'finalizada' | 'cancelada';
|
||||
|
||||
interface ParticipanteConfig {
|
||||
usuarioId: Id<'usuarios'>;
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
forcadoPeloAnfitriao?: boolean;
|
||||
}
|
||||
|
||||
interface ConfiguracoesChamada {
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
participantesConfig?: ParticipanteConfig[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Etapas no Código - Frontend Svelte
|
||||
|
||||
### Etapa 4: Instalar Dependências
|
||||
|
||||
**Arquivo:** `apps/web/package.json`
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
bun add lib-jitsi-meet
|
||||
```
|
||||
|
||||
**Dependências adicionais necessárias:**
|
||||
|
||||
- `lib-jitsi-meet` - Biblioteca oficial Jitsi
|
||||
- (Possivelmente tipos) `@types/lib-jitsi-meet` se disponível
|
||||
|
||||
### Etapa 5: Configurar Variáveis de Ambiente
|
||||
|
||||
**Arquivo:** `apps/web/.env`
|
||||
|
||||
```env
|
||||
# Jitsi Meet Configuration (Docker Local)
|
||||
VITE_JITSI_DOMAIN=localhost:8443
|
||||
VITE_JITSI_APP_ID=sgse-app
|
||||
VITE_JITSI_ROOM_PREFIX=sgse
|
||||
VITE_JITSI_USE_HTTPS=false
|
||||
```
|
||||
|
||||
### Etapa 6: Criar Utilitários Jitsi
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/utils/jitsi.ts`
|
||||
|
||||
**Funções:**
|
||||
|
||||
- `gerarRoomName(conversaId: string, tipo: "audio" | "video"): string` - Gerar nome único da sala
|
||||
- `obterConfiguracaoJitsi()` - Retornar configuração do Jitsi baseada em .env
|
||||
- `validarDispositivos()` - Validar disponibilidade de microfone/webcam
|
||||
- `obterDispositivosDisponiveis()` - Listar dispositivos de mídia
|
||||
|
||||
**Tipos (sem `any`):**
|
||||
|
||||
```typescript
|
||||
interface ConfiguracaoJitsi {
|
||||
domain: string;
|
||||
appId: string;
|
||||
roomPrefix: string;
|
||||
useHttps: boolean;
|
||||
}
|
||||
|
||||
interface DispositivoMedia {
|
||||
deviceId: string;
|
||||
label: string;
|
||||
kind: 'audioinput' | 'audiooutput' | 'videoinput';
|
||||
}
|
||||
|
||||
interface DispositivosDisponiveis {
|
||||
microphones: DispositivoMedia[];
|
||||
speakers: DispositivoMedia[];
|
||||
cameras: DispositivoMedia[];
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 7: Criar Store de Chamadas
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/stores/callStore.ts`
|
||||
|
||||
**Estado gerenciado:**
|
||||
|
||||
- Chamada ativa (se houver)
|
||||
- Estado de mídia (áudio/vídeo ligado/desligado)
|
||||
- Dispositivos selecionados
|
||||
- Status de gravação
|
||||
- Lista de participantes
|
||||
- Duração da chamada
|
||||
- É anfitrião ou não
|
||||
|
||||
**Tipos:**
|
||||
|
||||
```typescript
|
||||
interface EstadoChamada {
|
||||
chamadaId: Id<'chamadas'> | null;
|
||||
conversaId: Id<'conversas'> | null;
|
||||
tipo: 'audio' | 'video' | null;
|
||||
roomName: string | null;
|
||||
estaConectado: boolean;
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
gravando: boolean;
|
||||
ehAnfitriao: boolean;
|
||||
participantes: Array<{
|
||||
usuarioId: Id<'usuarios'>;
|
||||
nome: string;
|
||||
avatar?: string;
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
}>;
|
||||
duracaoSegundos: number;
|
||||
dispositivos: {
|
||||
microphoneId: string | null;
|
||||
cameraId: string | null;
|
||||
speakerId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface EventosChamada {
|
||||
'participant-joined': (participant: ParticipanteJitsi) => void;
|
||||
'participant-left': (participantId: string) => void;
|
||||
'audio-mute-status-changed': (isMuted: boolean) => void;
|
||||
'video-mute-status-changed': (isMuted: boolean) => void;
|
||||
'connection-failed': (error: Error) => void;
|
||||
'connection-disconnected': () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Métodos principais:**
|
||||
|
||||
- `iniciarChamada(conversaId, tipo)`
|
||||
- `finalizarChamada()`
|
||||
- `toggleAudio()`
|
||||
- `toggleVideo()`
|
||||
- `iniciarGravacao()`
|
||||
- `finalizarGravacao()`
|
||||
- `atualizarDispositivos()`
|
||||
|
||||
### Etapa 8: Criar Utilitários de Gravação
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/utils/mediaRecorder.ts`
|
||||
|
||||
**Funções:**
|
||||
|
||||
- `iniciarGravacaoAudio(stream: MediaStream): MediaRecorder` - Gravar apenas áudio
|
||||
- `iniciarGravacaoVideo(stream: MediaStream): MediaRecorder` - Gravar áudio + vídeo
|
||||
- `pararGravacao(recorder: MediaRecorder): Promise<Blob>` - Parar e retornar blob
|
||||
- `salvarGravacao(blob: Blob, nomeArquivo: string): void` - Salvar localmente
|
||||
- `obterDuracaoGravacao(recorder: MediaRecorder): number` - Obter duração
|
||||
|
||||
**Tipos:**
|
||||
|
||||
```typescript
|
||||
interface OpcoesGravacao {
|
||||
audioBitsPerSecond?: number;
|
||||
videoBitsPerSecond?: number;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
interface ResultadoGravacao {
|
||||
blob: Blob;
|
||||
duracaoSegundos: number;
|
||||
nomeArquivo: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 9: Criar Componente CallWindow
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/components/call/CallWindow.svelte`
|
||||
|
||||
**Características:**
|
||||
|
||||
- Janela flutuante redimensionável e arrastável
|
||||
- Integração com lib-jitsi-meet
|
||||
- Container para vídeo dos participantes
|
||||
- Barra de controles
|
||||
- Indicador de gravação
|
||||
- Contador de duração
|
||||
|
||||
**Props (TypeScript estrito):**
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
chamadaId: Id<'chamadas'>;
|
||||
conversaId: Id<'conversas'>;
|
||||
tipo: 'audio' | 'video';
|
||||
roomName: string;
|
||||
ehAnfitriao: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Estrutura:**
|
||||
|
||||
- `<script lang="ts">` com tipos explícitos
|
||||
- Uso de `$state`, `$derived`, `$effect` (Svelte 5)
|
||||
- Integração com `callStore`
|
||||
- Eventos do Jitsi tratados tipados
|
||||
|
||||
**Bibliotecas para janela flutuante:**
|
||||
|
||||
- Usar eventos nativos de mouse/touch para drag
|
||||
- CSS para redimensionamento com handles
|
||||
- localStorage para persistir posição/tamanho
|
||||
|
||||
### Etapa 10: Criar Componente CallControls
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/components/call/CallControls.svelte`
|
||||
|
||||
**Controles:**
|
||||
|
||||
- Botão toggle áudio
|
||||
- Botão toggle vídeo
|
||||
- Botão gravação (se anfitrião)
|
||||
- Botão configurações
|
||||
- Botão encerrar chamada
|
||||
- Contador de duração (HH:MM:SS)
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
gravando: boolean;
|
||||
ehAnfitriao: boolean;
|
||||
duracaoSegundos: number;
|
||||
onToggleAudio: () => void;
|
||||
onToggleVideo: () => void;
|
||||
onIniciarGravacao: () => void;
|
||||
onPararGravacao: () => void;
|
||||
onAbrirConfiguracoes: () => void;
|
||||
onEncerrar: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 11: Criar Componente CallSettings
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/components/call/CallSettings.svelte`
|
||||
|
||||
**Funcionalidades:**
|
||||
|
||||
- Listar microfones disponíveis
|
||||
- Listar webcams disponíveis
|
||||
- Listar alto-falantes disponíveis
|
||||
- Preview de vídeo antes de aplicar
|
||||
- Teste de áudio
|
||||
- Botões aplicar/cancelar
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
open: boolean;
|
||||
dispositivoAtual: {
|
||||
microphoneId: string | null;
|
||||
cameraId: string | null;
|
||||
speakerId: string | null;
|
||||
};
|
||||
onClose: () => void;
|
||||
onAplicar: (dispositivos: {
|
||||
microphoneId: string | null;
|
||||
cameraId: string | null;
|
||||
speakerId: string | null;
|
||||
}) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 12: Criar Componente HostControls
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/components/call/HostControls.svelte`
|
||||
|
||||
**Funcionalidades (apenas para anfitrião):**
|
||||
|
||||
- Lista de participantes
|
||||
- Toggle áudio por participante
|
||||
- Toggle vídeo por participante
|
||||
- Indicador visual de quem está gravando
|
||||
- Status de cada participante
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
participantes: Array<{
|
||||
usuarioId: Id<'usuarios'>;
|
||||
nome: string;
|
||||
avatar?: string;
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
forcadoPeloAnfitriao?: boolean;
|
||||
}>;
|
||||
onToggleParticipanteAudio: (usuarioId: Id<'usuarios'>) => void;
|
||||
onToggleParticipanteVideo: (usuarioId: Id<'usuarios'>) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 13: Criar Componente RecordingIndicator
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/components/call/RecordingIndicator.svelte`
|
||||
|
||||
**Características:**
|
||||
|
||||
- Banner visível no topo da janela
|
||||
- Ícone animado de gravação
|
||||
- Mensagem clara de que está gravando
|
||||
- Informação de quem iniciou (se disponível)
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
gravando: boolean;
|
||||
iniciadoPor?: string; // Nome do usuário que iniciou
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 14: Criar Utilitário de Janela Flutuante
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/utils/floatingWindow.ts`
|
||||
|
||||
**Funções:**
|
||||
|
||||
- `criarDragHandler(element: HTMLElement, handle: HTMLElement): () => void` - Criar handler de arrastar
|
||||
- `criarResizeHandler(element: HTMLElement, handles: HTMLElement[]): () => void` - Criar handler de redimensionar
|
||||
- `salvarPosicaoJanela(id: string, posicao: { x: number; y: number; width: number; height: number }): void` - Salvar no localStorage
|
||||
- `restaurarPosicaoJanela(id: string): { x: number; y: number; width: number; height: number } | null` - Restaurar do localStorage
|
||||
|
||||
**Tipos:**
|
||||
|
||||
```typescript
|
||||
interface PosicaoJanela {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface LimitesJanela {
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Etapa 15: Integrar com ChatWindow
|
||||
|
||||
**Arquivo:** `apps/web/src/lib/components/chat/ChatWindow.svelte`
|
||||
|
||||
**Modificações:**
|
||||
|
||||
- Adicionar botão de chamada de áudio
|
||||
- Adicionar botão de chamada de vídeo
|
||||
- Mostrar indicador quando há chamada ativa
|
||||
- Importar e usar CallWindow quando houver chamada
|
||||
|
||||
**Adicionar no topo (junto com outros botões):**
|
||||
|
||||
```svelte
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle"
|
||||
onclick={() => iniciarChamada('audio')}
|
||||
title="Ligação de áudio"
|
||||
>
|
||||
<Phone class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle"
|
||||
onclick={() => iniciarChamada('video')}
|
||||
title="Ligação de vídeo"
|
||||
>
|
||||
<Video class="h-4 w-4" />
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Ordem de Implementação Recomendada
|
||||
|
||||
1. ✅ **Etapa 1:** Configurar Docker Jitsi (fora do código)
|
||||
2. ✅ **Etapa 2:** Atualizar schema com tabela chamadas
|
||||
3. ✅ **Etapa 3:** Criar backend chamadas.ts com todas as funções
|
||||
4. ✅ **Etapa 4:** Instalar dependências frontend
|
||||
5. ✅ **Etapa 5:** Configurar variáveis de ambiente
|
||||
6. ✅ **Etapa 6:** Criar utilitários Jitsi (jitsi.ts)
|
||||
7. ✅ **Etapa 7:** Criar store de chamadas (callStore.ts)
|
||||
8. ✅ **Etapa 8:** Criar utilitários de gravação (mediaRecorder.ts)
|
||||
9. ✅ **Etapa 9:** Criar CallWindow básico (apenas estrutura)
|
||||
10. ✅ **Etapa 10:** Integrar lib-jitsi-meet no CallWindow
|
||||
11. ✅ **Etapa 11:** Criar CallControls e integrar
|
||||
12. ✅ **Etapa 12:** Implementar contador de duração
|
||||
13. ✅ **Etapa 13:** Implementar janela flutuante (drag & resize)
|
||||
14. ✅ **Etapa 14:** Criar CallSettings e integração de dispositivos
|
||||
15. ✅ **Etapa 15:** Criar HostControls e lógica de anfitrião
|
||||
16. ✅ **Etapa 16:** Implementar gravação local
|
||||
17. ✅ **Etapa 17:** Criar RecordingIndicator
|
||||
18. ✅ **Etapa 18:** Integrar botões no ChatWindow
|
||||
19. ✅ **Etapa 19:** Testes completos
|
||||
20. ✅ **Etapa 20:** Ajustes finais e tratamento de erros
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Segurança e Boas Práticas
|
||||
|
||||
### TypeScript
|
||||
|
||||
- ❌ **NUNCA** usar `any`
|
||||
- ✅ Usar tipos explícitos em todas as funções
|
||||
- ✅ Usar tipos inferidos do Convex quando possível
|
||||
- ✅ Criar interfaces para objetos complexos
|
||||
|
||||
### Svelte 5
|
||||
|
||||
- ✅ Usar `$props()` para props
|
||||
- ✅ Usar `$state()` para estado reativo
|
||||
- ✅ Usar `$derived()` para valores derivados
|
||||
- ✅ Usar `$effect()` para side effects
|
||||
|
||||
### Validação
|
||||
|
||||
- ✅ Validar permissões no backend antes de mutações
|
||||
- ✅ Validar entrada de dados
|
||||
- ✅ Tratar erros adequadamente
|
||||
- ✅ Logs de segurança (criação/finalização de chamadas)
|
||||
|
||||
### Performance
|
||||
|
||||
- ✅ Cleanup adequado de event listeners
|
||||
- ✅ Desconectar Jitsi ao fechar janela
|
||||
- ✅ Parar gravação ao finalizar chamada
|
||||
- ✅ Liberar streams de mídia
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notas Importantes
|
||||
|
||||
1. **Room Names:** Gerar room names únicos usando conversaId + timestamp + hash
|
||||
2. **Persistência:** Salvar posição/tamanho da janela no localStorage
|
||||
3. **Notificações:** Notificar participantes quando chamada é criada/finalizada
|
||||
4. **Limpeza:** Sempre limpar recursos ao finalizar chamada
|
||||
5. **Erros:** Tratar erros de conexão, permissões de mídia, etc.
|
||||
6. **Acessibilidade:** Adicionar labels, ARIA attributes, suporte a teclado
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testes
|
||||
|
||||
### Testes Funcionais
|
||||
|
||||
- [ ] Criar chamada de áudio individual
|
||||
- [ ] Criar chamada de vídeo individual
|
||||
- [ ] Criar chamada em grupo
|
||||
- [ ] Toggle áudio/vídeo
|
||||
- [ ] Anfitrião controlar participantes
|
||||
- [ ] Iniciar/parar gravação
|
||||
- [ ] Contador de duração
|
||||
- [ ] Configuração de dispositivos
|
||||
- [ ] Janela flutuante drag/resize
|
||||
|
||||
### Testes de Segurança
|
||||
|
||||
- [ ] Não anfitrião não pode controlar outros
|
||||
- [ ] Não anfitrião não pode iniciar gravação
|
||||
- [ ] Validação de participantes
|
||||
- [ ] Rate limiting de criação de chamadas
|
||||
|
||||
### Testes de Erros
|
||||
|
||||
- [ ] Conexão perdida
|
||||
- [ ] Sem permissão de mídia
|
||||
- [ ] Dispositivos não disponíveis
|
||||
- [ ] Servidor Jitsi offline
|
||||
|
||||
---
|
||||
|
||||
## 📚 Referências
|
||||
|
||||
- [Jitsi Meet Docker](https://github.com/jitsi/docker-jitsi-meet)
|
||||
- [lib-jitsi-meet Documentation](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-ljm-api)
|
||||
- [Svelte 5 Documentation](https://svelte.dev/docs)
|
||||
- [Convex Documentation](https://docs.convex.dev)
|
||||
- [WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API)
|
||||
- [MediaRecorder API](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)
|
||||
|
||||
---
|
||||
|
||||
**Data de Criação:** 2025-01-XX
|
||||
**Versão:** 1.0
|
||||
**Opção:** Docker Local (Desenvolvimento)
|
||||
@@ -1,117 +0,0 @@
|
||||
# Relatório de Testes - Sistema de Temas Personalizados
|
||||
|
||||
## Data: 2025-01-27
|
||||
|
||||
## Resumo Executivo
|
||||
Foram testados todos os 10 temas disponíveis no sistema SGSE através da aba "Aparência" na página de perfil. Cada tema foi selecionado e validado visualmente através de screenshots.
|
||||
|
||||
## Temas Testados
|
||||
|
||||
### 1. ✅ Tema Roxo (Purple)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema padrão com cores roxa e azul
|
||||
- **Screenshot**: `tema-roxo.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe cores roxas/azuis
|
||||
|
||||
### 2. ✅ Tema Azul (Blue)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema azul clássico e profissional
|
||||
- **Screenshot**: `tema-azul.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons de azul
|
||||
|
||||
### 3. ✅ Tema Verde (Green)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema verde natural e harmonioso
|
||||
- **Screenshot**: `tema-verde.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons de verde
|
||||
|
||||
### 4. ✅ Tema Laranja (Orange)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema laranja vibrante e energético
|
||||
- **Screenshot**: `tema-laranja.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons de laranja
|
||||
|
||||
### 5. ✅ Tema Vermelho (Red)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema vermelho intenso e impactante
|
||||
- **Screenshot**: `tema-vermelho.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons de vermelho
|
||||
|
||||
### 6. ✅ Tema Rosa (Pink)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema rosa suave e elegante
|
||||
- **Screenshot**: `tema-rosa.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons de rosa
|
||||
|
||||
### 7. ✅ Tema Verde-água (Teal)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema verde-água refrescante
|
||||
- **Screenshot**: `tema-verde-agua.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons de verde-água
|
||||
|
||||
### 8. ✅ Tema Escuro (Dark)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema escuro para uso noturno
|
||||
- **Screenshot**: `tema-escuro.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe fundo escuro
|
||||
|
||||
### 9. ✅ Tema Claro (Light)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema claro e minimalista
|
||||
- **Screenshot**: `tema-claro.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe fundo claro
|
||||
|
||||
### 10. ✅ Tema Corporativo (Corporate)
|
||||
- **Status**: Funcionando
|
||||
- **Descrição**: Tema corporativo azul escuro
|
||||
- **Screenshot**: `tema-corporativo.png`
|
||||
- **Observações**: Tema aplicado corretamente, interface exibe tons corporativos
|
||||
|
||||
## Funcionalidades Testadas
|
||||
|
||||
### ✅ Seleção de Temas
|
||||
- Todos os 10 temas podem ser selecionados através dos botões na interface
|
||||
- A seleção é visualmente indicada com "Tema Ativo"
|
||||
- A mudança de tema é aplicada imediatamente na interface
|
||||
|
||||
### ✅ Interface de Seleção
|
||||
- A aba "Aparência" está acessível na página de perfil
|
||||
- Todos os 10 temas são exibidos em cards com preview visual
|
||||
- Cada card mostra o nome, descrição e um gradiente de cores representativo
|
||||
|
||||
### ✅ Aplicação de Temas
|
||||
- Os temas são aplicados dinamicamente ao elemento `<html>` via atributo `data-theme`
|
||||
- As cores são alteradas em toda a interface (sidebar, header, botões, etc.)
|
||||
- A mudança é instantânea, sem necessidade de recarregar a página
|
||||
|
||||
## Screenshots Capturados
|
||||
|
||||
Todos os screenshots foram salvos com os seguintes nomes:
|
||||
- `tema-verde-agua-atual.png` - Estado inicial (tema verde-água)
|
||||
- `tema-roxo.png`
|
||||
- `tema-azul.png`
|
||||
- `tema-verde.png`
|
||||
- `tema-laranja.png`
|
||||
- `tema-vermelho.png`
|
||||
- `tema-rosa.png`
|
||||
- `tema-verde-agua.png`
|
||||
- `tema-escuro.png`
|
||||
- `tema-claro.png`
|
||||
- `tema-corporativo.png`
|
||||
|
||||
## Conclusão
|
||||
|
||||
✅ **Todos os 10 temas estão funcionando corretamente!**
|
||||
|
||||
- Cada tema altera a aparência da interface conforme esperado
|
||||
- As cores são aplicadas consistentemente em todos os componentes
|
||||
- A seleção de temas funciona de forma intuitiva e responsiva
|
||||
- O sistema está pronto para uso em produção
|
||||
|
||||
## Próximos Passos Recomendados
|
||||
|
||||
1. Testar a persistência do tema salvo no banco de dados
|
||||
2. Validar que o tema é aplicado automaticamente ao fazer login
|
||||
3. Verificar que o tema padrão (roxo) é aplicado ao fazer logout
|
||||
4. Testar com diferentes usuários para garantir isolamento de preferências
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Validação e Correções do Sistema de Temas
|
||||
|
||||
## Correções Implementadas
|
||||
|
||||
### 1. Temas Customizados Melhorados
|
||||
- Adicionadas todas as variáveis CSS necessárias do DaisyUI para cada tema customizado
|
||||
- Incluídas variáveis de arredondamento, animação e bordas
|
||||
- Adicionado `color-scheme` para temas claros/escuros
|
||||
|
||||
### 2. Estrutura Padronizada
|
||||
- Todos os temas customizados seguem o mesmo padrão de variáveis CSS
|
||||
- Temas nativos do DaisyUI (purple/aqua, dark, light) mantidos
|
||||
- Temas customizados (sgse-blue, sgse-green, etc.) com variáveis completas
|
||||
|
||||
### 3. Aplicação de Temas
|
||||
- Função `aplicarTema()` atualizada para aplicar corretamente no elemento HTML
|
||||
- Removido localStorage - tema salvo apenas no banco de dados
|
||||
- Tema padrão aplicado ao fazer logout
|
||||
|
||||
## Como Testar Manualmente
|
||||
|
||||
1. **Fazer Login:**
|
||||
- Email: `dfw@poli.br` / Senha: `Admin@2025`
|
||||
- OU Email: `kilder@kilder.com.br` / Senha: `Mudar@123`
|
||||
|
||||
2. **Acessar Página de Perfil:**
|
||||
- Clique no avatar do usuário no canto superior direito
|
||||
- Selecione "Meu Perfil"
|
||||
- OU acesse diretamente: `/perfil`
|
||||
|
||||
3. **Testar Cada Tema:**
|
||||
- Clique na aba "Aparência"
|
||||
- Teste cada um dos 10 temas:
|
||||
- **Roxo** (purple/aqua) - Padrão
|
||||
- **Azul** (sgse-blue)
|
||||
- **Verde** (sgse-green)
|
||||
- **Laranja** (sgse-orange)
|
||||
- **Vermelho** (sgse-red)
|
||||
- **Rosa** (sgse-pink)
|
||||
- **Verde-água** (sgse-teal)
|
||||
- **Escuro** (dark)
|
||||
- **Claro** (light)
|
||||
- **Corporativo** (sgse-corporate)
|
||||
|
||||
4. **Validar Mudanças:**
|
||||
- Ao clicar em um tema, a interface deve mudar imediatamente
|
||||
- Verificar cores em:
|
||||
- Sidebar
|
||||
- Botões
|
||||
- Cards
|
||||
- Badges
|
||||
- Links
|
||||
- Backgrounds
|
||||
|
||||
5. **Salvar Tema:**
|
||||
- Clique em "Salvar Tema" após selecionar
|
||||
- Faça logout e login novamente
|
||||
- O tema salvo deve ser aplicado automaticamente
|
||||
|
||||
6. **Testar Logout:**
|
||||
- Ao fazer logout, o tema deve voltar ao padrão (roxo)
|
||||
|
||||
## Problemas Identificados e Corrigidos
|
||||
|
||||
1. ✅ Variáveis CSS incompletas nos temas customizados
|
||||
2. ✅ Falta de `color-scheme` nos temas
|
||||
3. ✅ localStorage removido (tema apenas no banco)
|
||||
4. ✅ Tema padrão aplicado ao logout
|
||||
5. ✅ Estrutura padronizada de todos os temas
|
||||
|
||||
## Próximos Passos para Validação
|
||||
|
||||
Se algum tema não estiver funcionando:
|
||||
|
||||
1. Verificar no console do navegador (F12) se há erros
|
||||
2. Verificar o atributo `data-theme` no elemento `<html>` (deve mudar ao selecionar tema)
|
||||
3. Verificar se as variáveis CSS estão sendo aplicadas (DevTools > Elements > Computed)
|
||||
4. Testar em modo anônimo para garantir que não há cache
|
||||
|
||||
## Arquivos Modificados
|
||||
|
||||
- `apps/web/src/app.css` - Temas customizados melhorados
|
||||
- `apps/web/src/lib/utils/temas.ts` - Funções de aplicação de temas
|
||||
- `apps/web/src/routes/+layout.svelte` - Aplicação automática do tema
|
||||
- `apps/web/src/routes/(dashboard)/perfil/+page.svelte` - Interface de seleção
|
||||
- `apps/web/src/lib/components/Sidebar.svelte` - Reset de tema no logout
|
||||
- `packages/backend/convex/schema.ts` - Campo temaPreferido
|
||||
- `packages/backend/convex/usuarios.ts` - Função atualizarTema
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# ⚙️ Configuração de Variáveis de Ambiente
|
||||
|
||||
## 📁 Arquivo .env
|
||||
|
||||
Crie um arquivo `.env` na pasta `apps/web/` com as seguintes variáveis:
|
||||
|
||||
```env
|
||||
# Google Maps API Key (opcional)
|
||||
# Obtenha sua chave em: https://console.cloud.google.com/
|
||||
# Ative a "Geocoding API" para buscar coordenadas por endereço
|
||||
# Deixe vazio para usar OpenStreetMap (gratuito, sem necessidade de chave)
|
||||
VITE_GOOGLE_MAPS_API_KEY=
|
||||
|
||||
# VAPID Public Key para Push Notifications (opcional)
|
||||
VITE_VAPID_PUBLIC_KEY=
|
||||
```
|
||||
|
||||
## 📖 Documentação Completa
|
||||
|
||||
Para instruções detalhadas sobre como obter e configurar a Google Maps API Key, consulte:
|
||||
|
||||
📄 **[GOOGLE_MAPS_SETUP.md](./GOOGLE_MAPS_SETUP.md)**
|
||||
|
||||
## ⚠️ Importante
|
||||
|
||||
- O arquivo `.env` não deve ser commitado no Git (já está no .gitignore)
|
||||
- Variáveis de ambiente começam com `VITE_` para serem acessíveis no frontend
|
||||
- Reinicie o servidor de desenvolvimento após alterar o arquivo `.env`
|
||||
|
||||
72
apps/web/Dockerfile
Normal file
72
apps/web/Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
||||
# Use the official Bun image
|
||||
FROM oven/bun:1 AS base
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# ---
|
||||
FROM base AS prepare
|
||||
|
||||
RUN bun add -g turbo@^2
|
||||
COPY . .
|
||||
RUN turbo prune web --docker
|
||||
|
||||
|
||||
# ---
|
||||
FROM base AS builder
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY --from=prepare /app/out/json/ .
|
||||
RUN bun install
|
||||
# Build the project
|
||||
COPY --from=prepare /app/out/full/ .
|
||||
|
||||
ARG PUBLIC_CONVEX_URL
|
||||
ENV PUBLIC_CONVEX_URL=$PUBLIC_CONVEX_URL
|
||||
|
||||
ARG PUBLIC_CONVEX_SITE_URL
|
||||
ENV PUBLIC_CONVEX_SITE_URL=$PUBLIC_CONVEX_SITE_URL
|
||||
|
||||
RUN bunx turbo build
|
||||
|
||||
# Production stage
|
||||
FROM oven/bun:1-slim AS production
|
||||
|
||||
# Set working directory to match builder structure
|
||||
WORKDIR /app
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 sveltekit
|
||||
RUN adduser --system --uid 1001 sveltekit
|
||||
|
||||
# Copy root node_modules (contains hoisted dependencies)
|
||||
COPY --from=builder --chown=sveltekit:sveltekit /app/node_modules ./node_modules
|
||||
|
||||
# Copy built application and workspace files
|
||||
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/build ./apps/web/build
|
||||
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/package.json ./apps/web/package.json
|
||||
# Copy workspace node_modules (contains symlinks to root node_modules)
|
||||
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/node_modules ./apps/web/node_modules
|
||||
|
||||
# Copy any additional files needed for runtime
|
||||
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/static ./apps/web/static
|
||||
|
||||
# Switch to non-root user
|
||||
USER sveltekit
|
||||
|
||||
# Set working directory to the app
|
||||
WORKDIR /app/apps/web
|
||||
|
||||
# Expose the port that the app runs on
|
||||
EXPOSE 5173
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5173
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD bun --version || exit 1
|
||||
|
||||
# Start the application
|
||||
CMD ["bun", "./build/index.js"]
|
||||
@@ -1,174 +0,0 @@
|
||||
# 📍 Configuração do Google Maps API para Busca de Coordenadas
|
||||
|
||||
Este guia explica como configurar a API do Google Maps para obter coordenadas GPS de forma automática e precisa no sistema de Endereços de Marcação.
|
||||
|
||||
## 🎯 Por que usar Google Maps?
|
||||
|
||||
- ✅ **Maior Precisão**: Resultados mais exatos para endereços brasileiros
|
||||
- ✅ **Melhor Cobertura**: Banco de dados mais completo e atualizado
|
||||
- ✅ **Geocoding Avançado**: Entende melhor endereços incompletos ou parciais
|
||||
|
||||
> **Nota**: O sistema funciona perfeitamente sem a API key do Google Maps, usando OpenStreetMap (gratuito). A configuração do Google Maps é opcional.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Passo a Passo
|
||||
|
||||
### 1. Criar Projeto no Google Cloud Platform
|
||||
|
||||
1. Acesse [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Clique em **"Criar Projeto"** ou selecione um projeto existente
|
||||
3. Preencha o nome do projeto (ex: "SGSE-App")
|
||||
4. Clique em **"Criar"**
|
||||
|
||||
### 2. Ativar a Geocoding API
|
||||
|
||||
1. No menu lateral, vá em **"APIs e Serviços"** > **"Biblioteca"**
|
||||
2. Procure por **"Geocoding API"**
|
||||
3. Clique no resultado e depois em **"Ativar"**
|
||||
4. Aguarde alguns segundos para a ativação
|
||||
|
||||
### 3. Criar Chave de API
|
||||
|
||||
1. Ainda em **"APIs e Serviços"**, vá em **"Credenciais"**
|
||||
2. Clique em **"Criar Credenciais"** > **"Chave de API"**
|
||||
3. Copie a chave gerada (você precisará dela depois)
|
||||
|
||||
### 4. Configurar Restrições de Segurança (Recomendado)
|
||||
|
||||
Para proteger sua chave de API:
|
||||
|
||||
1. Clique na chave criada para editá-la
|
||||
2. Em **"Restrições de API"**:
|
||||
- Selecione **"Restringir chave"**
|
||||
- Escolha **"Geocoding API"**
|
||||
3. Em **"Restrições de aplicativo"**:
|
||||
- Para desenvolvimento local: escolha **"Referenciadores de sites HTTP"**
|
||||
- Adicione: `http://localhost:*` e `http://127.0.0.1:*`
|
||||
- Para produção: adicione o domínio do seu site
|
||||
4. Clique em **"Salvar"**
|
||||
|
||||
### 5. Configurar no Projeto
|
||||
|
||||
1. No diretório `apps/web/`, copie o arquivo de exemplo:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Abra o arquivo `.env` e adicione sua chave:
|
||||
```env
|
||||
VITE_GOOGLE_MAPS_API_KEY=sua_chave_aqui
|
||||
```
|
||||
|
||||
3. Reinicie o servidor de desenvolvimento:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 6. Verificar se está funcionando
|
||||
|
||||
1. Acesse a página de **Endereços de Marcação** (`/ti/configuracoes-ponto/enderecos`)
|
||||
2. Clique em **"Novo Endereço"**
|
||||
3. Preencha um endereço e clique em **"Buscar GPS"**
|
||||
4. Se configurado corretamente, verá a mensagem: *"Coordenadas encontradas via Google Maps!"*
|
||||
|
||||
---
|
||||
|
||||
## 💰 Custos
|
||||
|
||||
### Google Maps Geocoding API
|
||||
|
||||
- **$5.00 por 1.000 requisições** (primeiros 40.000 são gratuitos por mês)
|
||||
- **$0.005 por requisição** após os 40.000 gratuitos
|
||||
|
||||
> 💡 Para a maioria dos casos de uso, os 40.000 gratuitos são suficientes!
|
||||
|
||||
### OpenStreetMap (Fallback)
|
||||
|
||||
- **100% Gratuito** e ilimitado
|
||||
- Sem necessidade de configuração
|
||||
- Precisão levemente menor, mas ainda muito boa
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Como funciona o sistema
|
||||
|
||||
O sistema foi projetado para usar uma estratégia de **fallback inteligente**:
|
||||
|
||||
1. **Primeiro**: Tenta buscar via Google Maps (se API key configurada)
|
||||
2. **Se falhar ou não tiver API key**: Usa automaticamente OpenStreetMap
|
||||
3. **Feedback**: Informa qual serviço foi usado na mensagem de sucesso
|
||||
|
||||
Isso garante que o sistema sempre funcione, mesmo sem a API key do Google Maps.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Segurança
|
||||
|
||||
### ⚠️ Importante
|
||||
|
||||
- **Nunca** commite o arquivo `.env` no Git (já está no .gitignore)
|
||||
- **Nunca** compartilhe sua chave de API publicamente
|
||||
- Configure **restrições de API** no Google Cloud Console
|
||||
- Para produção, use variáveis de ambiente seguras no seu provedor de hospedagem
|
||||
|
||||
### Configuração em Produção
|
||||
|
||||
Para ambientes de produção (Vercel, Netlify, etc.):
|
||||
|
||||
1. Acesse as configurações do projeto no seu provedor
|
||||
2. Vá em **"Environment Variables"** ou **"Variáveis de Ambiente"**
|
||||
3. Adicione: `VITE_GOOGLE_MAPS_API_KEY` com o valor da sua chave
|
||||
4. Faça o deploy novamente
|
||||
|
||||
---
|
||||
|
||||
## ❓ Solução de Problemas
|
||||
|
||||
### A busca não está usando Google Maps
|
||||
|
||||
- Verifique se a variável `VITE_GOOGLE_MAPS_API_KEY` está no arquivo `.env`
|
||||
- Reinicie o servidor de desenvolvimento
|
||||
- Verifique no console do navegador se há erros
|
||||
|
||||
### Erro: "This API project is not authorized to use this API"
|
||||
|
||||
- Verifique se a **Geocoding API** está ativada no projeto
|
||||
- Aguarde alguns minutos após a ativação (pode levar até 5 minutos)
|
||||
|
||||
### Erro: "API key not valid"
|
||||
|
||||
- Verifique se copiou a chave corretamente
|
||||
- Verifique se as restrições de API permitem o uso da Geocoding API
|
||||
- Verifique se as restrições de aplicativo permitem seu domínio/endereço
|
||||
|
||||
### Mensagem: "Coordenadas encontradas via OpenStreetMap"
|
||||
|
||||
- Isso é normal se:
|
||||
- Não há API key configurada
|
||||
- A API key não é válida
|
||||
- O Google Maps falhou na busca
|
||||
- O sistema continua funcionando normalmente com OpenStreetMap
|
||||
|
||||
---
|
||||
|
||||
## 📚 Recursos Úteis
|
||||
|
||||
- [Google Cloud Console](https://console.cloud.google.com/)
|
||||
- [Documentação Geocoding API](https://developers.google.com/maps/documentation/geocoding)
|
||||
- [Preços Google Maps](https://developers.google.com/maps/billing-and-pricing/pricing)
|
||||
- [OpenStreetMap Nominatim](https://nominatim.org/)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Resumo
|
||||
|
||||
1. ✅ Crie projeto no Google Cloud
|
||||
2. ✅ Ative Geocoding API
|
||||
3. ✅ Crie chave de API
|
||||
4. ✅ Configure restrições (recomendado)
|
||||
5. ✅ Adicione `VITE_GOOGLE_MAPS_API_KEY` no `.env`
|
||||
6. ✅ Reinicie o servidor
|
||||
|
||||
**Pronto!** O sistema agora usará Google Maps para busca de coordenadas com maior precisão.
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"dev": "bunx --bun vite dev",
|
||||
"build": "bunx --bun vite build",
|
||||
"preview": "bunx --bun vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
@@ -22,6 +22,7 @@
|
||||
"esbuild": "^0.25.11",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.38.1",
|
||||
"svelte-adapter-bun": "^1.0.1",
|
||||
"svelte-check": "^4.3.1",
|
||||
"svelte-dnd-action": "^0.9.67",
|
||||
"tailwindcss": "^4.1.12",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import adapter from "@sveltejs/adapter-auto";
|
||||
import adapter from "svelte-adapter-bun";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
resolve: {
|
||||
dedupe: ["lucide-svelte"],
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["lib-jitsi-meet"], // Excluir para permitir carregamento dinâmico no browser
|
||||
},
|
||||
ssr: {
|
||||
noExternal: [], // lib-jitsi-meet não funciona no SSR, deve ser carregada apenas no browser
|
||||
},
|
||||
dedupe: ['lucide-svelte']
|
||||
}
|
||||
});
|
||||
|
||||
65
bun.lock
65
bun.lock
@@ -5,6 +5,7 @@
|
||||
"": {
|
||||
"name": "sgse-app",
|
||||
"dependencies": {
|
||||
"@convex-dev/better-auth": "^0.9.7",
|
||||
"@tanstack/svelte-form": "^1.23.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"lucide-svelte": "^0.552.0",
|
||||
@@ -67,6 +68,7 @@
|
||||
"esbuild": "^0.25.11",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.38.1",
|
||||
"svelte-adapter-bun": "^1.0.1",
|
||||
"svelte-check": "^4.3.1",
|
||||
"svelte-dnd-action": "^0.9.67",
|
||||
"tailwindcss": "^4.1.12",
|
||||
@@ -85,7 +87,6 @@
|
||||
"better-auth": "catalog:",
|
||||
"convex": "catalog:",
|
||||
"nodemailer": "^7.0.10",
|
||||
"ssh2": "^1.17.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sgse-app/eslint-config": "*",
|
||||
@@ -271,6 +272,12 @@
|
||||
|
||||
"@dicebear/thumbs": ["@dicebear/thumbs@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="],
|
||||
@@ -379,6 +386,8 @@
|
||||
|
||||
"@mmailaender/convex-better-auth-svelte": ["@mmailaender/convex-better-auth-svelte@0.2.0", "", { "dependencies": { "is-network-error": "^1.1.0" }, "peerDependencies": { "@convex-dev/better-auth": "^0.9.0", "better-auth": "^1.3.27", "convex": "^1.27.0", "convex-svelte": "^0.0.11", "svelte": "^5.0.0" } }, "sha512-qzahOJg30xErb4ZW+aeszQw4ydhCmKFXn8CeRSA77YxR/dDMgZl+vdWLE4EKsDN0Jd748ecWMnk1fDNNUdgDcg=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
|
||||
@@ -391,6 +400,10 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.71.0", "", {}, "sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w=="],
|
||||
|
||||
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="],
|
||||
|
||||
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="],
|
||||
@@ -417,6 +430,32 @@
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "x64" }, "sha512-40re4rMNrsi57oavRzIOpRGmg3QRlW6Ea8Q3znaqgOuJuKVrrm2bIQInTfkZJG7a4/5YMX7T951d0+toGLTdCA=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8BDM939bbMariZupiHp3OmP5N+LXPT4mULA0hZjDaq970PCxv4krZOSMG+HkWUUwmuQROtV+/00xw39EO0P+8g=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm" }, "sha512-sntsPaPgrECpBB/+2xrQzVUt0r493TMPI+4kWRMhvMsmrxOqH1Ep5lM0Wua/ZdbfZNwm1aVa5pcESQfNfM4Fhw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5clBW/I+er9F2uM1OFjJFWX86y7Lcy0M+NqsN4s3o07W+8467Zk8oQa4B45vdaXoNUF/yqIAgKkA/OEdQDxZqA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-wv+rnAfQDk9p/CheX8/Kmqk2o1WaFa4xhWI9gOyDMk/ljvOX0u0ubeM8nI1Qfox7Tnh71eV5AjzSePXUhFOyOg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-gxD0/xhU4Py47IH3bKZbWtvB99tMkUPGPJFRfSc5UB9Osoje0l0j1PPbxpUtXIELurYCqwLBKXIMTQGifox1BQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-HotuVe3XUjDwqqEMbm3o3IRkP9gdm8raY/btd/6KE3JGLF/cv4+3ff1l6nOhAZI8wulWDPEXPtE7v+HQEaTXnA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-8Cx+ucbd8n2dIr21FqBh6rUvTVL0uTgEtKR7l+MUZ5BgY4dFh1e4mPVX8oqmoYwOxBiXrsD2JIOCz4AyKLKxWA=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Vhq5vikrVDxAa75fxsyqj0c0Y/uti/TwshXI71Xb8IeUQJOBnmLUsn5dgYf5ljpYYkNa0z9BPAvUDIDMmyDi+w=="],
|
||||
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "ia32" }, "sha512-lN7RIg9Iugn08zP2aZN9y/MIdG8iOOCE93M1UrFlrxMTqPf8X+fDzmR/OKhTSd1A2pYNipZHjyTcb5H8kyQSow=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "x64" }, "sha512-7/7cLIn48Y+EpQ4CePvf8reFl63F15yPUlg4ZAhl+RXJIfydkdak1WD8Ir3AwAO+bJBXzrfNL+XQbxm0mcQZmw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="],
|
||||
@@ -605,6 +644,8 @@
|
||||
|
||||
"@tanstack/svelte-store": ["@tanstack/svelte-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-JeDyY7SxBi6EKzkf2wWoghdaC2bvmwNL9X/dgkx7LKEvJVle+te7tlELI3cqRNGbjXt9sx+97jx9M5dCCHcuog=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@1.0.0", "", { "dependencies": { "cookie": "*" } }, "sha512-mGFXbkDQJ6kAXByHS7QAggRXgols0mAdP4MuXgloGY1tXokvzaFFM4SMqWvf7AH0oafI7zlFJwoGWzmhDqTZ9w=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
@@ -653,6 +694,8 @@
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
@@ -671,8 +714,6 @@
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||
|
||||
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
@@ -689,8 +730,6 @@
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
||||
"better-auth": ["better-auth@1.3.27", "", { "dependencies": { "@better-auth/core": "1.3.27", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-SwiGAJ7yU6dBhNg0NdV1h5M8T5sa7/AszZVc4vBfMDrLLmvUfbt9JoJ0uRUJUEdKRAAxTyl9yA+F3+GhtAD80w=="],
|
||||
|
||||
"better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="],
|
||||
@@ -703,8 +742,6 @@
|
||||
|
||||
"browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="],
|
||||
|
||||
"buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
@@ -743,8 +780,6 @@
|
||||
|
||||
"core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="],
|
||||
|
||||
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
|
||||
@@ -1075,8 +1110,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nan": ["nan@2.23.1", "", {}, "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
|
||||
@@ -1191,6 +1224,8 @@
|
||||
|
||||
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@oxc-project/runtime": "0.71.0", "@oxc-project/types": "0.71.0", "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g=="],
|
||||
|
||||
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
|
||||
|
||||
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
|
||||
@@ -1207,8 +1242,6 @@
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
@@ -1237,8 +1270,6 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
|
||||
|
||||
"stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
@@ -1263,6 +1294,8 @@
|
||||
|
||||
"svelte": ["svelte@5.43.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA=="],
|
||||
|
||||
"svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="],
|
||||
|
||||
"svelte-chartjs": ["svelte-chartjs@3.1.5", "", { "peerDependencies": { "chart.js": "^3.5.0 || ^4.0.0", "svelte": "^4.0.0" } }, "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
|
||||
@@ -1307,8 +1340,6 @@
|
||||
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"lucide-svelte": "^0.552.0",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-sonner": "^1.0.5"
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"@convex-dev/better-auth": "^0.9.7"
|
||||
},
|
||||
"packageManager": "bun@1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/backend/.gitignore
vendored
3
packages/backend/.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
.env
|
||||
.env.local
|
||||
.env*
|
||||
.convex/
|
||||
|
||||
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
|
||||
import type * as actions_email from "../actions/email.js";
|
||||
import type * as actions_jitsiServer from "../actions/jitsiServer.js";
|
||||
import type * as actions_linkPreview from "../actions/linkPreview.js";
|
||||
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
|
||||
import type * as actions_smtp from "../actions/smtp.js";
|
||||
@@ -69,7 +68,6 @@ import type {
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
"actions/email": typeof actions_email;
|
||||
"actions/jitsiServer": typeof actions_jitsiServer;
|
||||
"actions/linkPreview": typeof actions_linkPreview;
|
||||
"actions/pushNotifications": typeof actions_pushNotifications;
|
||||
"actions/smtp": typeof actions_smtp;
|
||||
|
||||
@@ -1,432 +1,432 @@
|
||||
"use node";
|
||||
// "use node";
|
||||
|
||||
import { action } from "../_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { api, internal } from "../_generated/api";
|
||||
import { Client } from "ssh2";
|
||||
import { readFileSync } from "fs";
|
||||
import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
|
||||
// import { action } from "../_generated/server";
|
||||
// import { v } from "convex/values";
|
||||
// import { api, internal } from "../_generated/api";
|
||||
// import { Client } from "ssh2";
|
||||
// import { readFileSync } from "fs";
|
||||
// import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
|
||||
|
||||
/**
|
||||
* Interface para configuração SSH
|
||||
*/
|
||||
interface SSHConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
keyPath?: string;
|
||||
}
|
||||
// /**
|
||||
// * Interface para configuração SSH
|
||||
// */
|
||||
// interface SSHConfig {
|
||||
// host: string;
|
||||
// port: number;
|
||||
// username: string;
|
||||
// password?: string;
|
||||
// keyPath?: string;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Executar comando via SSH
|
||||
*/
|
||||
async function executarComandoSSH(
|
||||
config: SSHConfig,
|
||||
comando: string
|
||||
): Promise<{ sucesso: boolean; output: string; erro?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
let output = "";
|
||||
let errorOutput = "";
|
||||
// /**
|
||||
// * Executar comando via SSH
|
||||
// */
|
||||
// async function executarComandoSSH(
|
||||
// config: SSHConfig,
|
||||
// comando: string
|
||||
// ): Promise<{ sucesso: boolean; output: string; erro?: string }> {
|
||||
// return new Promise((resolve) => {
|
||||
// const conn = new Client();
|
||||
// let output = "";
|
||||
// let errorOutput = "";
|
||||
|
||||
conn.on("ready", () => {
|
||||
conn.exec(comando, (err, stream) => {
|
||||
if (err) {
|
||||
conn.end();
|
||||
resolve({ sucesso: false, output: "", erro: err.message });
|
||||
return;
|
||||
}
|
||||
// conn.on("ready", () => {
|
||||
// conn.exec(comando, (err, stream) => {
|
||||
// if (err) {
|
||||
// conn.end();
|
||||
// resolve({ sucesso: false, output: "", erro: err.message });
|
||||
// return;
|
||||
// }
|
||||
|
||||
stream
|
||||
.on("close", (code: number | null, signal: string | null) => {
|
||||
conn.end();
|
||||
if (code === 0) {
|
||||
resolve({ sucesso: true, output: output.trim() });
|
||||
} else {
|
||||
resolve({
|
||||
sucesso: false,
|
||||
output: output.trim(),
|
||||
erro: `Comando retornou código ${code}${signal ? ` (signal: ${signal})` : ""}. ${errorOutput}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
output += data.toString();
|
||||
})
|
||||
.stderr.on("data", (data: Buffer) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
});
|
||||
}).on("error", (err) => {
|
||||
resolve({ sucesso: false, output: "", erro: err.message });
|
||||
}).connect({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
privateKey: config.keyPath ? readFileSync(config.keyPath) : undefined,
|
||||
readyTimeout: 10000,
|
||||
});
|
||||
});
|
||||
}
|
||||
// stream
|
||||
// .on("close", (code: number | null, signal: string | null) => {
|
||||
// conn.end();
|
||||
// if (code === 0) {
|
||||
// resolve({ sucesso: true, output: output.trim() });
|
||||
// } else {
|
||||
// resolve({
|
||||
// sucesso: false,
|
||||
// output: output.trim(),
|
||||
// erro: `Comando retornou código ${code}${signal ? ` (signal: ${signal})` : ""}. ${errorOutput}`,
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .on("data", (data: Buffer) => {
|
||||
// output += data.toString();
|
||||
// })
|
||||
// .stderr.on("data", (data: Buffer) => {
|
||||
// errorOutput += data.toString();
|
||||
// });
|
||||
// });
|
||||
// }).on("error", (err) => {
|
||||
// resolve({ sucesso: false, output: "", erro: err.message });
|
||||
// }).connect({
|
||||
// host: config.host,
|
||||
// port: config.port,
|
||||
// username: config.username,
|
||||
// password: config.password,
|
||||
// privateKey: config.keyPath ? readFileSync(config.keyPath) : undefined,
|
||||
// readyTimeout: 10000,
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Ler arquivo via SSH
|
||||
*/
|
||||
async function lerArquivoSSH(
|
||||
config: SSHConfig,
|
||||
caminho: string
|
||||
): Promise<{ sucesso: boolean; conteudo?: string; erro?: string }> {
|
||||
const comando = `cat "${caminho}" 2>&1`;
|
||||
const resultado = await executarComandoSSH(config, comando);
|
||||
// /**
|
||||
// * Ler arquivo via SSH
|
||||
// */
|
||||
// async function lerArquivoSSH(
|
||||
// config: SSHConfig,
|
||||
// caminho: string
|
||||
// ): Promise<{ sucesso: boolean; conteudo?: string; erro?: string }> {
|
||||
// const comando = `cat "${caminho}" 2>&1`;
|
||||
// const resultado = await executarComandoSSH(config, comando);
|
||||
|
||||
if (!resultado.sucesso) {
|
||||
return { sucesso: false, erro: resultado.erro || "Erro ao ler arquivo" };
|
||||
}
|
||||
// if (!resultado.sucesso) {
|
||||
// return { sucesso: false, erro: resultado.erro || "Erro ao ler arquivo" };
|
||||
// }
|
||||
|
||||
return { sucesso: true, conteudo: resultado.output };
|
||||
}
|
||||
// return { sucesso: true, conteudo: resultado.output };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Escrever arquivo via SSH
|
||||
*/
|
||||
async function escreverArquivoSSH(
|
||||
config: SSHConfig,
|
||||
caminho: string,
|
||||
conteudo: string
|
||||
): Promise<{ sucesso: boolean; erro?: string }> {
|
||||
// Escapar conteúdo para shell
|
||||
const conteudoEscapado = conteudo
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, "\\$")
|
||||
.replace(/`/g, "\\`");
|
||||
// /**
|
||||
// * Escrever arquivo via SSH
|
||||
// */
|
||||
// async function escreverArquivoSSH(
|
||||
// config: SSHConfig,
|
||||
// caminho: string,
|
||||
// conteudo: string
|
||||
// ): Promise<{ sucesso: boolean; erro?: string }> {
|
||||
// // Escapar conteúdo para shell
|
||||
// const conteudoEscapado = conteudo
|
||||
// .replace(/\\/g, "\\\\")
|
||||
// .replace(/"/g, '\\"')
|
||||
// .replace(/\$/g, "\\$")
|
||||
// .replace(/`/g, "\\`");
|
||||
|
||||
const comando = `cat > "${caminho}" << 'JITSI_CONFIG_EOF'
|
||||
${conteudo}
|
||||
JITSI_CONFIG_EOF`;
|
||||
// const comando = `cat > "${caminho}" << 'JITSI_CONFIG_EOF'
|
||||
// ${conteudo}
|
||||
// JITSI_CONFIG_EOF`;
|
||||
|
||||
const resultado = await executarComandoSSH(config, comando);
|
||||
// const resultado = await executarComandoSSH(config, comando);
|
||||
|
||||
if (!resultado.sucesso) {
|
||||
return { sucesso: false, erro: resultado.erro || "Erro ao escrever arquivo" };
|
||||
}
|
||||
// if (!resultado.sucesso) {
|
||||
// return { sucesso: false, erro: resultado.erro || "Erro ao escrever arquivo" };
|
||||
// }
|
||||
|
||||
return { sucesso: true };
|
||||
}
|
||||
// return { sucesso: true };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Aplicar configurações do Jitsi no servidor Docker via SSH
|
||||
*/
|
||||
export const aplicarConfiguracaoServidor = action({
|
||||
args: {
|
||||
configId: v.id("configuracaoJitsi"),
|
||||
sshPassword: v.optional(v.string()), // Senha SSH (se não usar chave)
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
sucesso: v.literal(true),
|
||||
mensagem: v.string(),
|
||||
detalhes: v.optional(v.string()),
|
||||
}),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<
|
||||
| { sucesso: true; mensagem: string; detalhes?: string }
|
||||
| { sucesso: false; erro: string }
|
||||
> => {
|
||||
try {
|
||||
// Buscar configuração
|
||||
const config = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {});
|
||||
// /**
|
||||
// * Aplicar configurações do Jitsi no servidor Docker via SSH
|
||||
// */
|
||||
// export const aplicarConfiguracaoServidor = action({
|
||||
// args: {
|
||||
// configId: v.id("configuracaoJitsi"),
|
||||
// sshPassword: v.optional(v.string()), // Senha SSH (se não usar chave)
|
||||
// },
|
||||
// returns: v.union(
|
||||
// v.object({
|
||||
// sucesso: v.literal(true),
|
||||
// mensagem: v.string(),
|
||||
// detalhes: v.optional(v.string()),
|
||||
// }),
|
||||
// v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
// ),
|
||||
// handler: async (ctx, args): Promise<
|
||||
// | { sucesso: true; mensagem: string; detalhes?: string }
|
||||
// | { sucesso: false; erro: string }
|
||||
// > => {
|
||||
// try {
|
||||
// // Buscar configuração
|
||||
// const config = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {});
|
||||
|
||||
if (!config || config._id !== args.configId) {
|
||||
return { sucesso: false as const, erro: "Configuração não encontrada" };
|
||||
}
|
||||
// if (!config || config._id !== args.configId) {
|
||||
// return { sucesso: false as const, erro: "Configuração não encontrada" };
|
||||
// }
|
||||
|
||||
// Verificar se tem configurações SSH
|
||||
const configFull = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsiCompleta, {
|
||||
configId: args.configId,
|
||||
});
|
||||
// // Verificar se tem configurações SSH
|
||||
// const configFull = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsiCompleta, {
|
||||
// configId: args.configId,
|
||||
// });
|
||||
|
||||
if (!configFull || !configFull.sshHost) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Configurações SSH não estão definidas. Configure o servidor SSH primeiro.",
|
||||
};
|
||||
}
|
||||
// if (!configFull || !configFull.sshHost) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: "Configurações SSH não estão definidas. Configure o servidor SSH primeiro.",
|
||||
// };
|
||||
// }
|
||||
|
||||
// Configurar SSH
|
||||
let sshPasswordDecrypted: string | undefined = undefined;
|
||||
// // Configurar SSH
|
||||
// let sshPasswordDecrypted: string | undefined = undefined;
|
||||
|
||||
// Se senha foi fornecida, usar ela. Caso contrário, tentar descriptografar a armazenada
|
||||
if (args.sshPassword) {
|
||||
sshPasswordDecrypted = args.sshPassword;
|
||||
} else if (configFull.sshPasswordHash && configFull.sshPasswordHash !== "********") {
|
||||
// Tentar descriptografar senha armazenada
|
||||
try {
|
||||
sshPasswordDecrypted = await decryptSMTPPasswordNode(configFull.sshPasswordHash);
|
||||
} catch (error) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Não foi possível descriptografar a senha SSH armazenada. Forneça a senha novamente.",
|
||||
};
|
||||
}
|
||||
}
|
||||
// // Se senha foi fornecida, usar ela. Caso contrário, tentar descriptografar a armazenada
|
||||
// if (args.sshPassword) {
|
||||
// sshPasswordDecrypted = args.sshPassword;
|
||||
// } else if (configFull.sshPasswordHash && configFull.sshPasswordHash !== "********") {
|
||||
// // Tentar descriptografar senha armazenada
|
||||
// try {
|
||||
// sshPasswordDecrypted = await decryptSMTPPasswordNode(configFull.sshPasswordHash);
|
||||
// } catch (error) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: "Não foi possível descriptografar a senha SSH armazenada. Forneça a senha novamente.",
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
const sshConfig: SSHConfig = {
|
||||
host: configFull.sshHost,
|
||||
port: configFull.sshPort || 22,
|
||||
username: configFull.sshUsername || "",
|
||||
password: sshPasswordDecrypted,
|
||||
keyPath: configFull.sshKeyPath || undefined,
|
||||
};
|
||||
// const sshConfig: SSHConfig = {
|
||||
// host: configFull.sshHost,
|
||||
// port: configFull.sshPort || 22,
|
||||
// username: configFull.sshUsername || "",
|
||||
// password: sshPasswordDecrypted,
|
||||
// keyPath: configFull.sshKeyPath || undefined,
|
||||
// };
|
||||
|
||||
if (!sshConfig.username) {
|
||||
return { sucesso: false as const, erro: "Usuário SSH não configurado" };
|
||||
}
|
||||
// if (!sshConfig.username) {
|
||||
// return { sucesso: false as const, erro: "Usuário SSH não configurado" };
|
||||
// }
|
||||
|
||||
if (!sshConfig.password && !sshConfig.keyPath) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Senha SSH ou caminho da chave deve ser fornecido",
|
||||
};
|
||||
}
|
||||
// if (!sshConfig.password && !sshConfig.keyPath) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: "Senha SSH ou caminho da chave deve ser fornecido",
|
||||
// };
|
||||
// }
|
||||
|
||||
const basePath = configFull.jitsiConfigPath || "~/.jitsi-meet-cfg";
|
||||
const dockerComposePath = configFull.dockerComposePath || ".";
|
||||
// const basePath = configFull.jitsiConfigPath || "~/.jitsi-meet-cfg";
|
||||
// const dockerComposePath = configFull.dockerComposePath || ".";
|
||||
|
||||
// Extrair host e porta do domain
|
||||
const [host, portStr] = configFull.domain.split(":");
|
||||
const port = portStr ? parseInt(portStr, 10) : configFull.useHttps ? 443 : 80;
|
||||
const protocol = configFull.useHttps ? "https" : "http";
|
||||
// // Extrair host e porta do domain
|
||||
// const [host, portStr] = configFull.domain.split(":");
|
||||
// const port = portStr ? parseInt(portStr, 10) : configFull.useHttps ? 443 : 80;
|
||||
// const protocol = configFull.useHttps ? "https" : "http";
|
||||
|
||||
const detalhes: string[] = [];
|
||||
// const detalhes: string[] = [];
|
||||
|
||||
// 1. Atualizar arquivo .env do docker-compose
|
||||
if (dockerComposePath) {
|
||||
const envContent = `# Configuração Jitsi - Atualizada automaticamente pelo SGSE
|
||||
CONFIG=${basePath}
|
||||
TZ=America/Recife
|
||||
ENABLE_LETSENCRYPT=0
|
||||
HTTP_PORT=${protocol === "https" ? 8000 : port}
|
||||
HTTPS_PORT=${configFull.useHttps ? port : 8443}
|
||||
PUBLIC_URL=${protocol}://${host}${portStr ? `:${port}` : ""}
|
||||
DOMAIN=${host}
|
||||
ENABLE_AUTH=0
|
||||
ENABLE_GUESTS=1
|
||||
ENABLE_TRANSCRIPTION=0
|
||||
ENABLE_RECORDING=0
|
||||
ENABLE_PREJOIN_PAGE=0
|
||||
START_AUDIO_MUTED=0
|
||||
START_VIDEO_MUTED=0
|
||||
ENABLE_XMPP_WEBSOCKET=0
|
||||
ENABLE_P2P=1
|
||||
MAX_NUMBER_OF_PARTICIPANTS=10
|
||||
RESOLUTION_WIDTH=1280
|
||||
RESOLUTION_HEIGHT=720
|
||||
JWT_APP_ID=${configFull.appId}
|
||||
JWT_APP_SECRET=
|
||||
`;
|
||||
// // 1. Atualizar arquivo .env do docker-compose
|
||||
// if (dockerComposePath) {
|
||||
// const envContent = `# Configuração Jitsi - Atualizada automaticamente pelo SGSE
|
||||
// CONFIG=${basePath}
|
||||
// TZ=America/Recife
|
||||
// ENABLE_LETSENCRYPT=0
|
||||
// HTTP_PORT=${protocol === "https" ? 8000 : port}
|
||||
// HTTPS_PORT=${configFull.useHttps ? port : 8443}
|
||||
// PUBLIC_URL=${protocol}://${host}${portStr ? `:${port}` : ""}
|
||||
// DOMAIN=${host}
|
||||
// ENABLE_AUTH=0
|
||||
// ENABLE_GUESTS=1
|
||||
// ENABLE_TRANSCRIPTION=0
|
||||
// ENABLE_RECORDING=0
|
||||
// ENABLE_PREJOIN_PAGE=0
|
||||
// START_AUDIO_MUTED=0
|
||||
// START_VIDEO_MUTED=0
|
||||
// ENABLE_XMPP_WEBSOCKET=0
|
||||
// ENABLE_P2P=1
|
||||
// MAX_NUMBER_OF_PARTICIPANTS=10
|
||||
// RESOLUTION_WIDTH=1280
|
||||
// RESOLUTION_HEIGHT=720
|
||||
// JWT_APP_ID=${configFull.appId}
|
||||
// JWT_APP_SECRET=
|
||||
// `;
|
||||
|
||||
const envPath = `${dockerComposePath}/.env`;
|
||||
const resultadoEnv = await escreverArquivoSSH(sshConfig, envPath, envContent);
|
||||
// const envPath = `${dockerComposePath}/.env`;
|
||||
// const resultadoEnv = await escreverArquivoSSH(sshConfig, envPath, envContent);
|
||||
|
||||
if (!resultadoEnv.sucesso) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao atualizar .env: ${resultadoEnv.erro}`,
|
||||
};
|
||||
}
|
||||
// if (!resultadoEnv.sucesso) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao atualizar .env: ${resultadoEnv.erro}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
detalhes.push(`✓ Arquivo .env atualizado: ${envPath}`);
|
||||
}
|
||||
// detalhes.push(`✓ Arquivo .env atualizado: ${envPath}`);
|
||||
// }
|
||||
|
||||
// 2. Atualizar configuração do Prosody (conforme documentação oficial)
|
||||
const prosodyConfigPath = `${basePath}/prosody/config/${host}.cfg.lua`;
|
||||
const prosodyContent = `-- Configuração Prosody para ${host}
|
||||
-- Gerada automaticamente pelo SGSE
|
||||
-- Baseado na documentação oficial do Jitsi Meet
|
||||
// // 2. Atualizar configuração do Prosody (conforme documentação oficial)
|
||||
// const prosodyConfigPath = `${basePath}/prosody/config/${host}.cfg.lua`;
|
||||
// const prosodyContent = `-- Configuração Prosody para ${host}
|
||||
// -- Gerada automaticamente pelo SGSE
|
||||
// -- Baseado na documentação oficial do Jitsi Meet
|
||||
|
||||
VirtualHost "${host}"
|
||||
authentication = "anonymous"
|
||||
modules_enabled = {
|
||||
"bosh";
|
||||
"websocket";
|
||||
"ping";
|
||||
"speakerstats";
|
||||
"turncredentials";
|
||||
"presence";
|
||||
"conference_duration";
|
||||
"stats";
|
||||
}
|
||||
c2s_require_encryption = false
|
||||
allow_anonymous_s2s = false
|
||||
bosh_max_inactivity = 60
|
||||
bosh_max_polling = 5
|
||||
bosh_max_stanzas = 5
|
||||
// VirtualHost "${host}"
|
||||
// authentication = "anonymous"
|
||||
// modules_enabled = {
|
||||
// "bosh";
|
||||
// "websocket";
|
||||
// "ping";
|
||||
// "speakerstats";
|
||||
// "turncredentials";
|
||||
// "presence";
|
||||
// "conference_duration";
|
||||
// "stats";
|
||||
// }
|
||||
// c2s_require_encryption = false
|
||||
// allow_anonymous_s2s = false
|
||||
// bosh_max_inactivity = 60
|
||||
// bosh_max_polling = 5
|
||||
// bosh_max_stanzas = 5
|
||||
|
||||
Component "conference.${host}" "muc"
|
||||
storage = "memory"
|
||||
muc_room_locking = false
|
||||
muc_room_default_public_jids = true
|
||||
muc_room_cache_size = 1000
|
||||
muc_log_presences = true
|
||||
// Component "conference.${host}" "muc"
|
||||
// storage = "memory"
|
||||
// muc_room_locking = false
|
||||
// muc_room_default_public_jids = true
|
||||
// muc_room_cache_size = 1000
|
||||
// muc_log_presences = true
|
||||
|
||||
Component "jitsi-videobridge.${host}"
|
||||
component_secret = ""
|
||||
// Component "jitsi-videobridge.${host}"
|
||||
// component_secret = ""
|
||||
|
||||
Component "focus.${host}"
|
||||
component_secret = ""
|
||||
`;
|
||||
// Component "focus.${host}"
|
||||
// component_secret = ""
|
||||
// `;
|
||||
|
||||
const resultadoProsody = await escreverArquivoSSH(sshConfig, prosodyConfigPath, prosodyContent);
|
||||
// const resultadoProsody = await escreverArquivoSSH(sshConfig, prosodyConfigPath, prosodyContent);
|
||||
|
||||
if (!resultadoProsody.sucesso) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao atualizar Prosody: ${resultadoProsody.erro}`,
|
||||
};
|
||||
}
|
||||
// if (!resultadoProsody.sucesso) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao atualizar Prosody: ${resultadoProsody.erro}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
detalhes.push(`✓ Configuração Prosody atualizada: ${prosodyConfigPath}`);
|
||||
// detalhes.push(`✓ Configuração Prosody atualizada: ${prosodyConfigPath}`);
|
||||
|
||||
// 3. Atualizar configuração do Jicofo
|
||||
const jicofoConfigPath = `${basePath}/jicofo/sip-communicator.properties`;
|
||||
const jicofoContent = `# Configuração Jicofo
|
||||
# Gerada automaticamente pelo SGSE
|
||||
org.jitsi.jicofo.BRIDGE_MUC=JvbBrewery@internal.${host}
|
||||
org.jitsi.jicofo.jid=XMPP_USER@${host}
|
||||
org.jitsi.jicofo.BRIDGE_MUC_JID=MUC_BRIDGE_JID@internal.${host}
|
||||
org.jitsi.jicofo.app.ID=${configFull.appId}
|
||||
`;
|
||||
// // 3. Atualizar configuração do Jicofo
|
||||
// const jicofoConfigPath = `${basePath}/jicofo/sip-communicator.properties`;
|
||||
// const jicofoContent = `# Configuração Jicofo
|
||||
// # Gerada automaticamente pelo SGSE
|
||||
// org.jitsi.jicofo.BRIDGE_MUC=JvbBrewery@internal.${host}
|
||||
// org.jitsi.jicofo.jid=XMPP_USER@${host}
|
||||
// org.jitsi.jicofo.BRIDGE_MUC_JID=MUC_BRIDGE_JID@internal.${host}
|
||||
// org.jitsi.jicofo.app.ID=${configFull.appId}
|
||||
// `;
|
||||
|
||||
const resultadoJicofo = await escreverArquivoSSH(sshConfig, jicofoConfigPath, jicofoContent);
|
||||
// const resultadoJicofo = await escreverArquivoSSH(sshConfig, jicofoConfigPath, jicofoContent);
|
||||
|
||||
if (!resultadoJicofo.sucesso) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao atualizar Jicofo: ${resultadoJicofo.erro}`,
|
||||
};
|
||||
}
|
||||
// if (!resultadoJicofo.sucesso) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao atualizar Jicofo: ${resultadoJicofo.erro}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
detalhes.push(`✓ Configuração Jicofo atualizada: ${jicofoConfigPath}`);
|
||||
// detalhes.push(`✓ Configuração Jicofo atualizada: ${jicofoConfigPath}`);
|
||||
|
||||
// 4. Atualizar configuração do JVB
|
||||
const jvbConfigPath = `${basePath}/jvb/sip-communicator.properties`;
|
||||
const jvbContent = `# Configuração JVB (Jitsi Video Bridge)
|
||||
# Gerada automaticamente pelo SGSE
|
||||
org.jitsi.videobridge.AUTHORIZED_SOURCE_REGEXP=.*@${host}/.*
|
||||
org.jitsi.videobridge.xmpp.user.shard.HOSTNAME=${host}
|
||||
org.jitsi.videobridge.xmpp.user.shard.DOMAIN=auth.${host}
|
||||
org.jitsi.videobridge.xmpp.user.shard.USERNAME=jvb
|
||||
org.jitsi.videobridge.xmpp.user.shard.MUC_JIDS=JvbBrewery@internal.${host}
|
||||
`;
|
||||
// // 4. Atualizar configuração do JVB
|
||||
// const jvbConfigPath = `${basePath}/jvb/sip-communicator.properties`;
|
||||
// const jvbContent = `# Configuração JVB (Jitsi Video Bridge)
|
||||
// # Gerada automaticamente pelo SGSE
|
||||
// org.jitsi.videobridge.AUTHORIZED_SOURCE_REGEXP=.*@${host}/.*
|
||||
// org.jitsi.videobridge.xmpp.user.shard.HOSTNAME=${host}
|
||||
// org.jitsi.videobridge.xmpp.user.shard.DOMAIN=auth.${host}
|
||||
// org.jitsi.videobridge.xmpp.user.shard.USERNAME=jvb
|
||||
// org.jitsi.videobridge.xmpp.user.shard.MUC_JIDS=JvbBrewery@internal.${host}
|
||||
// `;
|
||||
|
||||
const resultadoJvb = await escreverArquivoSSH(sshConfig, jvbConfigPath, jvbContent);
|
||||
// const resultadoJvb = await escreverArquivoSSH(sshConfig, jvbConfigPath, jvbContent);
|
||||
|
||||
if (!resultadoJvb.sucesso) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao atualizar JVB: ${resultadoJvb.erro}`,
|
||||
};
|
||||
}
|
||||
// if (!resultadoJvb.sucesso) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao atualizar JVB: ${resultadoJvb.erro}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
detalhes.push(`✓ Configuração JVB atualizada: ${jvbConfigPath}`);
|
||||
// detalhes.push(`✓ Configuração JVB atualizada: ${jvbConfigPath}`);
|
||||
|
||||
// 5. Reiniciar containers Docker
|
||||
if (dockerComposePath) {
|
||||
const resultadoRestart = await executarComandoSSH(
|
||||
sshConfig,
|
||||
`cd "${dockerComposePath}" && docker-compose restart 2>&1 || docker compose restart 2>&1`
|
||||
);
|
||||
// // 5. Reiniciar containers Docker
|
||||
// if (dockerComposePath) {
|
||||
// const resultadoRestart = await executarComandoSSH(
|
||||
// sshConfig,
|
||||
// `cd "${dockerComposePath}" && docker-compose restart 2>&1 || docker compose restart 2>&1`
|
||||
// );
|
||||
|
||||
if (!resultadoRestart.sucesso) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao reiniciar containers: ${resultadoRestart.erro}`,
|
||||
};
|
||||
}
|
||||
// if (!resultadoRestart.sucesso) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao reiniciar containers: ${resultadoRestart.erro}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
detalhes.push(`✓ Containers Docker reiniciados`);
|
||||
}
|
||||
// detalhes.push(`✓ Containers Docker reiniciados`);
|
||||
// }
|
||||
|
||||
// Atualizar timestamp de configuração no servidor
|
||||
await ctx.runMutation(internal.configuracaoJitsi.marcarConfiguradoNoServidor, {
|
||||
configId: args.configId,
|
||||
});
|
||||
// // Atualizar timestamp de configuração no servidor
|
||||
// await ctx.runMutation(internal.configuracaoJitsi.marcarConfiguradoNoServidor, {
|
||||
// configId: args.configId,
|
||||
// });
|
||||
|
||||
return {
|
||||
sucesso: true as const,
|
||||
mensagem: "Configurações aplicadas com sucesso no servidor Jitsi",
|
||||
detalhes: detalhes.join("\n"),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao aplicar configurações: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
// return {
|
||||
// sucesso: true as const,
|
||||
// mensagem: "Configurações aplicadas com sucesso no servidor Jitsi",
|
||||
// detalhes: detalhes.join("\n"),
|
||||
// };
|
||||
// } catch (error: unknown) {
|
||||
// const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao aplicar configurações: ${errorMessage}`,
|
||||
// };
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
/**
|
||||
* Testar conexão SSH
|
||||
*/
|
||||
export const testarConexaoSSH = action({
|
||||
args: {
|
||||
sshHost: v.string(),
|
||||
sshPort: v.optional(v.number()),
|
||||
sshUsername: v.string(),
|
||||
sshPassword: v.optional(v.string()),
|
||||
sshKeyPath: v.optional(v.string()),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), mensagem: v.string() }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<
|
||||
| { sucesso: true; mensagem: string }
|
||||
| { sucesso: false; erro: string }
|
||||
> => {
|
||||
try {
|
||||
if (!args.sshPassword && !args.sshKeyPath) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Senha SSH ou caminho da chave deve ser fornecido",
|
||||
};
|
||||
}
|
||||
// /**
|
||||
// * Testar conexão SSH
|
||||
// */
|
||||
// export const testarConexaoSSH = action({
|
||||
// args: {
|
||||
// sshHost: v.string(),
|
||||
// sshPort: v.optional(v.number()),
|
||||
// sshUsername: v.string(),
|
||||
// sshPassword: v.optional(v.string()),
|
||||
// sshKeyPath: v.optional(v.string()),
|
||||
// },
|
||||
// returns: v.union(
|
||||
// v.object({ sucesso: v.literal(true), mensagem: v.string() }),
|
||||
// v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
// ),
|
||||
// handler: async (ctx, args): Promise<
|
||||
// | { sucesso: true; mensagem: string }
|
||||
// | { sucesso: false; erro: string }
|
||||
// > => {
|
||||
// try {
|
||||
// if (!args.sshPassword && !args.sshKeyPath) {
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: "Senha SSH ou caminho da chave deve ser fornecido",
|
||||
// };
|
||||
// }
|
||||
|
||||
const sshConfig: SSHConfig = {
|
||||
host: args.sshHost,
|
||||
port: args.sshPort || 22,
|
||||
username: args.sshUsername,
|
||||
password: args.sshPassword || undefined,
|
||||
keyPath: args.sshKeyPath || undefined,
|
||||
};
|
||||
// const sshConfig: SSHConfig = {
|
||||
// host: args.sshHost,
|
||||
// port: args.sshPort || 22,
|
||||
// username: args.sshUsername,
|
||||
// password: args.sshPassword || undefined,
|
||||
// keyPath: args.sshKeyPath || undefined,
|
||||
// };
|
||||
|
||||
// Tentar executar um comando simples
|
||||
const resultado = await executarComandoSSH(sshConfig, "echo 'SSH_OK'");
|
||||
// // Tentar executar um comando simples
|
||||
// const resultado = await executarComandoSSH(sshConfig, "echo 'SSH_OK'");
|
||||
|
||||
if (resultado.sucesso && resultado.output.includes("SSH_OK")) {
|
||||
return {
|
||||
sucesso: true as const,
|
||||
mensagem: "Conexão SSH estabelecida com sucesso",
|
||||
};
|
||||
}
|
||||
// if (resultado.sucesso && resultado.output.includes("SSH_OK")) {
|
||||
// return {
|
||||
// sucesso: true as const,
|
||||
// mensagem: "Conexão SSH estabelecida com sucesso",
|
||||
// };
|
||||
// }
|
||||
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: resultado.erro || "Falha ao estabelecer conexão SSH",
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao testar SSH: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: resultado.erro || "Falha ao estabelecer conexão SSH",
|
||||
// };
|
||||
// } catch (error: unknown) {
|
||||
// const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// return {
|
||||
// sucesso: false as const,
|
||||
// erro: `Erro ao testar SSH: ${errorMessage}`,
|
||||
// };
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
|
||||
@@ -891,6 +891,9 @@ export const getInstanceWithSteps = query({
|
||||
startedAt: number | undefined;
|
||||
finishedAt: number | undefined;
|
||||
notes: string | undefined;
|
||||
notesUpdatedBy: Id<'usuarios'> | undefined;
|
||||
notesUpdatedByName: string | undefined;
|
||||
notesUpdatedAt: number | undefined;
|
||||
dueDate: number | undefined;
|
||||
position: number;
|
||||
expectedDuration: number;
|
||||
@@ -929,6 +932,7 @@ export const getInstanceWithSteps = query({
|
||||
});
|
||||
}
|
||||
|
||||
const notesUpdater = step.notesUpdatedBy ? await ctx.db.get(step.notesUpdatedBy) : null;
|
||||
stepsWithDetails.push({
|
||||
_id: step._id,
|
||||
_creationTime: step._creationTime,
|
||||
@@ -945,7 +949,7 @@ export const getInstanceWithSteps = query({
|
||||
finishedAt: step.finishedAt,
|
||||
notes: step.notes,
|
||||
notesUpdatedBy: step.notesUpdatedBy,
|
||||
notesUpdatedByName: step.notesUpdatedBy ? (await ctx.db.get(step.notesUpdatedBy))?.nome : undefined,
|
||||
notesUpdatedByName: notesUpdater?.nome,
|
||||
notesUpdatedAt: step.notesUpdatedAt,
|
||||
dueDate: step.dueDate,
|
||||
position: flowStep?.position ?? 0,
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"better-auth": "catalog:",
|
||||
"convex": "catalog:",
|
||||
"nodemailer": "^7.0.10",
|
||||
"ssh2": "^1.17.0"
|
||||
"nodemailer": "^7.0.10"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": ["dist/**"]
|
||||
"outputs": ["build/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
|
||||
Reference in New Issue
Block a user