feat: enhance time synchronization and Jitsi configuration handling
- Implemented a comprehensive time synchronization mechanism that applies GMT offsets based on user configuration, ensuring accurate timestamps across the application. - Updated the Jitsi configuration to include SSH settings, allowing for better integration with Docker setups. - Refactored the backend queries and mutations to handle the new SSH configuration fields, ensuring secure and flexible server management. - Enhanced error handling and logging for time synchronization processes, providing clearer feedback for users and developers.
This commit is contained in:
@@ -1,52 +1,90 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
|
||||
const client = useConvexClient();
|
||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||
const configAtual = useQuery(api.configuracaoJitsi.obterConfigJitsi, {});
|
||||
|
||||
let domain = $state("");
|
||||
let appId = $state("sgse-app");
|
||||
let roomPrefix = $state("sgse");
|
||||
// Query condicional para configuração completa
|
||||
const configCompletaQuery = $derived(
|
||||
configAtual?.data?._id ? { configId: configAtual.data._id } : null
|
||||
);
|
||||
const configCompleta = useQuery(
|
||||
api.configuracaoJitsi.obterConfigJitsiCompleta,
|
||||
configCompletaQuery ? configCompletaQuery : 'skip'
|
||||
);
|
||||
|
||||
let domain = $state('');
|
||||
let appId = $state('sgse-app');
|
||||
let roomPrefix = $state('sgse');
|
||||
let useHttps = $state(false);
|
||||
let acceptSelfSignedCert = $state(false);
|
||||
|
||||
// Campos SSH/Docker
|
||||
let sshHost = $state('');
|
||||
let sshPort = $state(22);
|
||||
let sshUsername = $state('');
|
||||
let sshPassword = $state('');
|
||||
let sshKeyPath = $state('');
|
||||
let dockerComposePath = $state('');
|
||||
let jitsiConfigPath = $state('~/.jitsi-meet-cfg');
|
||||
|
||||
let mostrarConfigSSH = $state(false);
|
||||
let processando = $state(false);
|
||||
let testando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
|
||||
let testandoSSH = $state(false);
|
||||
let aplicandoServidor = $state(false);
|
||||
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string; detalhes?: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
function mostrarMensagem(tipo: 'success' | 'error', texto: string, detalhes?: string) {
|
||||
mensagem = { tipo, texto, detalhes };
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 5000);
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
// Carregar config existente
|
||||
$effect(() => {
|
||||
if (configAtual?.data) {
|
||||
domain = configAtual.data.domain || "";
|
||||
appId = configAtual.data.appId || "sgse-app";
|
||||
roomPrefix = configAtual.data.roomPrefix || "sgse";
|
||||
domain = configAtual.data.domain || '';
|
||||
appId = configAtual.data.appId || 'sgse-app';
|
||||
roomPrefix = configAtual.data.roomPrefix || 'sgse';
|
||||
useHttps = configAtual.data.useHttps ?? false;
|
||||
acceptSelfSignedCert = configAtual.data.acceptSelfSignedCert ?? false;
|
||||
} else if (configAtual === null) {
|
||||
// Se não há configuração, resetar para valores padrão
|
||||
domain = "";
|
||||
appId = "sgse-app";
|
||||
roomPrefix = "sgse";
|
||||
domain = '';
|
||||
appId = 'sgse-app';
|
||||
roomPrefix = 'sgse';
|
||||
useHttps = false;
|
||||
acceptSelfSignedCert = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Carregar configurações SSH/Docker
|
||||
$effect(() => {
|
||||
if (configCompleta?.data) {
|
||||
sshHost = configCompleta.data.sshHost || '';
|
||||
sshPort = configCompleta.data.sshPort || 22;
|
||||
sshUsername = configCompleta.data.sshUsername || '';
|
||||
sshPassword = ''; // Sempre limpar senha por segurança
|
||||
sshKeyPath = configCompleta.data.sshKeyPath || '';
|
||||
dockerComposePath = configCompleta.data.dockerComposePath || '';
|
||||
jitsiConfigPath = configCompleta.data.jitsiConfigPath || '~/.jitsi-meet-cfg';
|
||||
mostrarConfigSSH = !!(configCompleta.data.sshHost || configCompleta.data.sshUsername);
|
||||
}
|
||||
});
|
||||
|
||||
// Ativar HTTPS automaticamente se domínio contém porta 8443
|
||||
$effect(() => {
|
||||
if (domain.includes(":8443")) {
|
||||
if (domain.includes(':8443')) {
|
||||
useHttps = true;
|
||||
// Para localhost com porta 8443, geralmente é certificado autoassinado
|
||||
if (domain.includes("localhost")) {
|
||||
if (domain.includes('localhost')) {
|
||||
acceptSelfSignedCert = true;
|
||||
}
|
||||
}
|
||||
@@ -55,22 +93,19 @@
|
||||
async function salvarConfiguracao() {
|
||||
// Validação de campos obrigatórios
|
||||
if (!domain?.trim() || !appId?.trim() || !roomPrefix?.trim()) {
|
||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||
mostrarMensagem('error', 'Preencha todos os campos obrigatórios');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validação de roomPrefix (apenas letras, números e hífens)
|
||||
const roomPrefixRegex = /^[a-zA-Z0-9-]+$/;
|
||||
if (!roomPrefixRegex.test(roomPrefix.trim())) {
|
||||
mostrarMensagem(
|
||||
"error",
|
||||
"Prefixo de sala deve conter apenas letras, números e hífens"
|
||||
);
|
||||
mostrarMensagem('error', 'Prefixo de sala deve conter apenas letras, números e hífens');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUser?.data) {
|
||||
mostrarMensagem("error", "Usuário não autenticado");
|
||||
mostrarMensagem('error', 'Usuário não autenticado');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,18 +117,26 @@
|
||||
roomPrefix: roomPrefix.trim(),
|
||||
useHttps,
|
||||
acceptSelfSignedCert,
|
||||
configuradoPorId: currentUser.data._id as Id<"usuarios">,
|
||||
configuradoPorId: currentUser.data._id as Id<'usuarios'>,
|
||||
// Configurações SSH/Docker (opcionais)
|
||||
sshHost: sshHost.trim() || undefined,
|
||||
sshPort: sshPort || undefined,
|
||||
sshUsername: sshUsername.trim() || undefined,
|
||||
sshPassword: sshPassword.trim() || undefined,
|
||||
sshKeyPath: sshKeyPath.trim() || undefined,
|
||||
dockerComposePath: dockerComposePath.trim() || undefined,
|
||||
jitsiConfigPath: jitsiConfigPath.trim() || undefined
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem("success", "Configuração salva com sucesso!");
|
||||
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
||||
} else {
|
||||
mostrarMensagem("error", resultado.erro);
|
||||
mostrarMensagem('error', resultado.erro);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error("Erro ao salvar configuração:", error);
|
||||
mostrarMensagem("error", errorMessage || "Erro ao salvar configuração");
|
||||
console.error('Erro ao salvar configuração:', error);
|
||||
mostrarMensagem('error', errorMessage || 'Erro ao salvar configuração');
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
@@ -101,7 +144,7 @@
|
||||
|
||||
async function testarConexao() {
|
||||
if (!domain?.trim()) {
|
||||
mostrarMensagem("error", "Preencha o domínio antes de testar");
|
||||
mostrarMensagem('error', 'Preencha o domínio antes de testar');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,45 +153,128 @@
|
||||
const resultado = await client.action(api.configuracaoJitsi.testarConexaoJitsi, {
|
||||
domain: domain.trim(),
|
||||
useHttps,
|
||||
acceptSelfSignedCert,
|
||||
acceptSelfSignedCert
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
const mensagemSucesso = resultado.aviso
|
||||
const mensagemSucesso = resultado.aviso
|
||||
? `Conexão testada com sucesso! ${resultado.aviso}`
|
||||
: "Conexão testada com sucesso! Servidor Jitsi está acessível.";
|
||||
mostrarMensagem("success", mensagemSucesso);
|
||||
: 'Conexão testada com sucesso! Servidor Jitsi está acessível.';
|
||||
mostrarMensagem('success', mensagemSucesso);
|
||||
} else {
|
||||
mostrarMensagem("error", `Erro ao testar conexão: ${resultado.erro}`);
|
||||
mostrarMensagem('error', `Erro ao testar conexão: ${resultado.erro}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error("Erro ao testar conexão:", error);
|
||||
mostrarMensagem(
|
||||
"error",
|
||||
errorMessage || "Erro ao conectar com o servidor Jitsi"
|
||||
);
|
||||
console.error('Erro ao testar conexão:', error);
|
||||
mostrarMensagem('error', errorMessage || 'Erro ao conectar com o servidor Jitsi');
|
||||
} finally {
|
||||
testando = false;
|
||||
}
|
||||
}
|
||||
|
||||
const statusConfig = $derived(
|
||||
configAtual?.data?.ativo ? "Configurado" : "Não configurado"
|
||||
);
|
||||
async function testarConexaoSSH() {
|
||||
if (!sshHost?.trim() || !sshUsername?.trim()) {
|
||||
mostrarMensagem('error', 'Preencha Host e Usuário SSH antes de testar');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sshPassword?.trim() && !sshKeyPath?.trim()) {
|
||||
mostrarMensagem('error', 'Preencha a senha SSH ou o caminho da chave antes de testar');
|
||||
return;
|
||||
}
|
||||
|
||||
testandoSSH = true;
|
||||
try {
|
||||
const resultado = await client.action(api.actions.jitsiServer.testarConexaoSSH, {
|
||||
sshHost: sshHost.trim(),
|
||||
sshPort: sshPort || 22,
|
||||
sshUsername: sshUsername.trim(),
|
||||
sshPassword: sshPassword.trim() || undefined,
|
||||
sshKeyPath: sshKeyPath.trim() || undefined
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem('success', resultado.mensagem);
|
||||
} else {
|
||||
mostrarMensagem('error', `Erro ao testar SSH: ${resultado.erro}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('Erro ao testar SSH:', error);
|
||||
mostrarMensagem('error', errorMessage || 'Erro ao conectar via SSH');
|
||||
} finally {
|
||||
testandoSSH = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function aplicarConfiguracaoServidor() {
|
||||
if (!configAtual?.data?._id) {
|
||||
mostrarMensagem('error', 'Salve a configuração básica antes de aplicar no servidor');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sshHost?.trim() || !sshUsername?.trim()) {
|
||||
mostrarMensagem('error', 'Configure o acesso SSH antes de aplicar no servidor');
|
||||
return;
|
||||
}
|
||||
|
||||
// Senha SSH é necessária para aplicar (pode ser a armazenada ou uma nova)
|
||||
if (!sshPassword?.trim() && !sshKeyPath?.trim() && !configCompleta?.data?.sshPasswordHash) {
|
||||
mostrarMensagem(
|
||||
'error',
|
||||
'Forneça a senha SSH ou o caminho da chave para aplicar a configuração'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
'Deseja aplicar essas configurações no servidor Jitsi Docker? Os containers serão reiniciados.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
aplicandoServidor = true;
|
||||
try {
|
||||
const resultado = await client.action(api.actions.jitsiServer.aplicarConfiguracaoServidor, {
|
||||
configId: configAtual.data._id,
|
||||
sshPassword: sshPassword.trim() || undefined
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem('success', resultado.mensagem, resultado.detalhes);
|
||||
// Limpar senha após uso
|
||||
sshPassword = '';
|
||||
} else {
|
||||
mostrarMensagem('error', `Erro ao aplicar configuração: ${resultado.erro}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('Erro ao aplicar configuração:', error);
|
||||
mostrarMensagem('error', errorMessage || 'Erro ao aplicar configuração no servidor');
|
||||
} finally {
|
||||
aplicandoServidor = false;
|
||||
}
|
||||
}
|
||||
|
||||
const statusConfig = $derived(configAtual?.data?.ativo ? 'Configurado' : 'Não configurado');
|
||||
|
||||
const configuradoNoServidor = $derived(configCompleta?.data?.configuradoNoServidor ?? false);
|
||||
|
||||
const isLoading = $derived(configAtual === undefined);
|
||||
const hasError = $derived(configAtual === null && !isLoading);
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
||||
<div class="container mx-auto max-w-4xl px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<div class="bg-primary/10 rounded-xl p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-primary"
|
||||
class="text-primary h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -162,7 +288,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Configurações do Jitsi Meet</h1>
|
||||
<h1 class="text-base-content text-3xl font-bold">Configurações do Jitsi Meet</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Configurar servidor Jitsi para chamadas de vídeo e áudio
|
||||
</p>
|
||||
@@ -174,16 +300,16 @@
|
||||
{#if mensagem}
|
||||
<div
|
||||
class="alert mb-6"
|
||||
class:alert-success={mensagem.tipo === "success"}
|
||||
class:alert-error={mensagem.tipo === "error"}
|
||||
class:alert-success={mensagem.tipo === 'success'}
|
||||
class:alert-error={mensagem.tipo === 'error'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{#if mensagem.tipo === "success"}
|
||||
{#if mensagem.tipo === 'success'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -199,7 +325,16 @@
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{mensagem.texto}</span>
|
||||
<div class="flex-1">
|
||||
<span>{mensagem.texto}</span>
|
||||
{#if mensagem.detalhes}
|
||||
<details class="mt-2">
|
||||
<summary class="cursor-pointer text-sm opacity-75">Detalhes</summary>
|
||||
<pre
|
||||
class="bg-base-200 mt-2 rounded p-2 font-mono text-xs whitespace-pre-wrap">{mensagem.detalhes}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -213,16 +348,12 @@
|
||||
|
||||
<!-- Status -->
|
||||
{#if !isLoading}
|
||||
<div
|
||||
class="alert {configAtual?.data?.ativo
|
||||
? 'alert-success'
|
||||
: 'alert-warning'} mb-6"
|
||||
>
|
||||
<div class="alert {configAtual?.data?.ativo ? 'alert-success' : 'alert-warning'} mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
{#if configAtual?.data?.ativo}
|
||||
<path
|
||||
@@ -244,9 +375,7 @@
|
||||
<strong>Status:</strong>
|
||||
{statusConfig}
|
||||
{#if configAtual?.data?.testadoEm}
|
||||
- Última conexão testada em {new Date(
|
||||
configAtual.data.testadoEm
|
||||
).toLocaleString("pt-BR")}
|
||||
- Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -258,7 +387,7 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Dados do Servidor Jitsi</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- Domínio -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="jitsi-domain">
|
||||
@@ -308,30 +437,25 @@
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Apenas letras, números e hífens</span
|
||||
>
|
||||
<span class="label-text-alt">Apenas letras, números e hífens</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opções de Segurança -->
|
||||
<div class="divider"></div>
|
||||
<h3 class="font-bold mb-2">Configurações de Segurança</h3>
|
||||
<h3 class="mb-2 font-bold">Configurações de Segurança</h3>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={useHttps}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<input type="checkbox" bind:checked={useHttps} class="checkbox checkbox-primary" />
|
||||
<span class="label-text font-medium">Usar HTTPS</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Ativado automaticamente se domínio contém :8443. Desmarque para usar HTTP (não recomendado para produção)</span
|
||||
>Ativado automaticamente se domínio contém :8443. Desmarque para usar HTTP (não
|
||||
recomendado para produção)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,14 +471,228 @@
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning"
|
||||
>Habilitar apenas para desenvolvimento local com certificados autoassinados. Em produção, use certificados válidos.</span
|
||||
>Habilitar apenas para desenvolvimento local com certificados autoassinados. Em
|
||||
produção, use certificados válidos.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configurações SSH/Docker -->
|
||||
<div class="divider"></div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h3 class="font-bold">Configuração SSH/Docker (Opcional)</h3>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<span class="label-text text-sm">Configurar servidor via SSH</span>
|
||||
<input type="checkbox" bind:checked={mostrarConfigSSH} class="checkbox checkbox-sm" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if mostrarConfigSSH}
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- SSH Host -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="ssh-host">
|
||||
<span class="label-text font-medium">Host SSH *</span>
|
||||
</label>
|
||||
<input
|
||||
id="ssh-host"
|
||||
type="text"
|
||||
bind:value={sshHost}
|
||||
placeholder="192.168.1.100 ou servidor.local"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Endereço do servidor Docker</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH Port -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="ssh-port">
|
||||
<span class="label-text font-medium">Porta SSH</span>
|
||||
</label>
|
||||
<input
|
||||
id="ssh-port"
|
||||
type="number"
|
||||
bind:value={sshPort}
|
||||
placeholder="22"
|
||||
min="1"
|
||||
max="65535"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- SSH Username -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="ssh-username">
|
||||
<span class="label-text font-medium">Usuário SSH *</span>
|
||||
</label>
|
||||
<input
|
||||
id="ssh-username"
|
||||
type="text"
|
||||
bind:value={sshUsername}
|
||||
placeholder="usuario"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- SSH Password ou Key Path -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="ssh-password">
|
||||
<span class="label-text font-medium">Senha SSH</span>
|
||||
</label>
|
||||
<input
|
||||
id="ssh-password"
|
||||
type="password"
|
||||
bind:value={sshPassword}
|
||||
placeholder="Deixe vazio para manter senha salva"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Ou use caminho da chave SSH abaixo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH Key Path -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="ssh-key-path">
|
||||
<span class="label-text font-medium">Caminho da Chave SSH</span>
|
||||
</label>
|
||||
<input
|
||||
id="ssh-key-path"
|
||||
type="text"
|
||||
bind:value={sshKeyPath}
|
||||
placeholder="/home/usuario/.ssh/id_rsa"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Caminho no servidor SSH para a chave privada</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Compose Path -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="docker-compose-path">
|
||||
<span class="label-text font-medium">Caminho Docker Compose</span>
|
||||
</label>
|
||||
<input
|
||||
id="docker-compose-path"
|
||||
type="text"
|
||||
bind:value={dockerComposePath}
|
||||
placeholder="/home/usuario/jitsi-docker"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Diretório com docker-compose.yml</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jitsi Config Path -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="jitsi-config-path">
|
||||
<span class="label-text font-medium">Caminho Config Jitsi</span>
|
||||
</label>
|
||||
<input
|
||||
id="jitsi-config-path"
|
||||
type="text"
|
||||
bind:value={jitsiConfigPath}
|
||||
placeholder="~/.jitsi-meet-cfg"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Diretório de configurações do Jitsi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Configuração Servidor -->
|
||||
{#if configuradoNoServidor}
|
||||
<div class="alert alert-success mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Configuração aplicada no servidor
|
||||
{#if configCompleta?.data?.configuradoNoServidorEm}
|
||||
em {new Date(configCompleta.data.configuradoNoServidorEm).toLocaleString('pt-BR')}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Botões SSH/Docker -->
|
||||
<div class="mt-4 flex gap-3">
|
||||
<button
|
||||
class="btn btn-outline btn-info"
|
||||
onclick={testarConexaoSSH}
|
||||
disabled={testandoSSH || processando || aplicandoServidor}
|
||||
>
|
||||
{#if testandoSSH}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
Testar SSH
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-success"
|
||||
onclick={aplicarConfiguracaoServidor}
|
||||
disabled={aplicandoServidor ||
|
||||
processando ||
|
||||
testando ||
|
||||
testandoSSH ||
|
||||
!configAtual?.data?._id}
|
||||
>
|
||||
{#if aplicandoServidor}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
Aplicar no Servidor Docker
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="card-actions justify-end mt-6 gap-3">
|
||||
<div class="card-actions mt-6 justify-end gap-3">
|
||||
<button
|
||||
class="btn btn-outline btn-info"
|
||||
onclick={testarConexao}
|
||||
@@ -412,12 +750,12 @@
|
||||
{/if}
|
||||
|
||||
<!-- Exemplos Comuns -->
|
||||
<div class="card bg-base-100 shadow-xl mt-6">
|
||||
<div class="card bg-base-100 mt-6 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Exemplos de Configuração</h2>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<table class="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ambiente</th>
|
||||
@@ -461,7 +799,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -473,12 +811,12 @@
|
||||
<div>
|
||||
<p>
|
||||
<strong>Dica:</strong> Para servidor Jitsi Docker local, use
|
||||
<code>localhost:8443</code> com HTTPS habilitado. Para servidor em
|
||||
produção, use o domínio completo do seu servidor Jitsi.
|
||||
<code>localhost:8443</code> com HTTPS habilitado. Para servidor em produção, use o domínio completo
|
||||
do seu servidor Jitsi.
|
||||
</p>
|
||||
<p class="text-sm mt-1">
|
||||
A configuração será aplicada imediatamente após salvar. Usuários precisarão
|
||||
recarregar a página para usar a nova configuração.
|
||||
<p class="mt-1 text-sm">
|
||||
A configuração será aplicada imediatamente após salvar. Usuários precisarão recarregar a
|
||||
página para usar a nova configuração.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -490,7 +828,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -501,10 +839,10 @@
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold">Certificados Autoassinados Ativados</p>
|
||||
<p class="text-sm mt-1">
|
||||
Para certificados autoassinados (desenvolvimento local), os usuários precisarão
|
||||
aceitar o certificado no navegador na primeira conexão. Em produção, use
|
||||
certificados válidos (Let's Encrypt, etc.).
|
||||
<p class="mt-1 text-sm">
|
||||
Para certificados autoassinados (desenvolvimento local), os usuários precisarão aceitar o
|
||||
certificado no navegador na primeira conexão. Em produção, use certificados válidos (Let's
|
||||
Encrypt, etc.).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,7 +855,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -528,12 +866,11 @@
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold">HTTP Ativado (Não Seguro)</p>
|
||||
<p class="text-sm mt-1">
|
||||
O uso de HTTP não é recomendado para produção. Use HTTPS com certificado válido
|
||||
para garantir segurança nas chamadas.
|
||||
<p class="mt-1 text-sm">
|
||||
O uso de HTTP não é recomendado para produção. Use HTTPS com certificado válido para
|
||||
garantir segurança nas chamadas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,10 +10,20 @@
|
||||
let portaNTP = $state(123);
|
||||
let usarServidorExterno = $state(false);
|
||||
let fallbackParaPC = $state(true);
|
||||
let gmtOffset = $state(0);
|
||||
let gmtOffset = $state(-3); // Padrão GMT-3 para Brasília
|
||||
let processando = $state(false);
|
||||
let testando = $state(false);
|
||||
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
||||
let statusSincronizacao = $state<{
|
||||
ultimaSincronizacao: number | null;
|
||||
offsetSegundos: number | null;
|
||||
usandoServidorExterno: boolean;
|
||||
} | null>(null);
|
||||
|
||||
// Estados para os relógios
|
||||
let timestampOriginal = $state<number | null>(null);
|
||||
let timestampUTC = $state<number | null>(null);
|
||||
let intervaloRelogio: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
$effect(() => {
|
||||
if (configQuery?.data) {
|
||||
@@ -21,10 +31,159 @@
|
||||
portaNTP = configQuery.data.portaNTP || 123;
|
||||
usarServidorExterno = configQuery.data.usarServidorExterno || false;
|
||||
fallbackParaPC = configQuery.data.fallbackParaPC !== undefined ? configQuery.data.fallbackParaPC : true;
|
||||
gmtOffset = configQuery.data.gmtOffset ?? 0;
|
||||
gmtOffset = configQuery.data.gmtOffset ?? -3; // Padrão GMT-3 para Brasília
|
||||
|
||||
// Atualizar status de sincronização
|
||||
statusSincronizacao = {
|
||||
ultimaSincronizacao: configQuery.data.ultimaSincronizacao ?? null,
|
||||
offsetSegundos: configQuery.data.offsetSegundos ?? null,
|
||||
usandoServidorExterno: configQuery.data.usarServidorExterno || false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Função para obter tempo sincronizado
|
||||
async function obterTempoSincronizado() {
|
||||
try {
|
||||
if (usarServidorExterno) {
|
||||
// Se usar servidor externo, sincronizar com NTP
|
||||
const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {});
|
||||
if (resultado.sucesso) {
|
||||
timestampUTC = resultado.timestamp; // Timestamp UTC da fonte
|
||||
// Calcular o timestamp original (antes do ajuste GMT)
|
||||
timestampOriginal = timestampUTC;
|
||||
}
|
||||
} else {
|
||||
// Se não usar servidor externo, usar tempo local do PC (igual ao relógio de ponto)
|
||||
timestampUTC = Date.now();
|
||||
timestampOriginal = timestampUTC;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter tempo sincronizado:', error);
|
||||
// Fallback: usar tempo local
|
||||
timestampUTC = Date.now();
|
||||
timestampOriginal = timestampUTC;
|
||||
}
|
||||
}
|
||||
|
||||
// Inicializar e atualizar relógios periodicamente
|
||||
$effect(() => {
|
||||
// Obter tempo inicial quando configuração mudar
|
||||
if (configQuery?.data) {
|
||||
obterTempoSincronizado();
|
||||
}
|
||||
|
||||
// Atualizar a cada segundo
|
||||
intervaloRelogio = setInterval(() => {
|
||||
if (timestampUTC !== null) {
|
||||
// Incrementar em 1 segundo
|
||||
timestampUTC += 1000;
|
||||
timestampOriginal = timestampUTC;
|
||||
} else {
|
||||
// Se ainda não temos timestamp, tentar obter novamente
|
||||
obterTempoSincronizado();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Limpar intervalo quando componente for destruído
|
||||
return () => {
|
||||
if (intervaloRelogio) {
|
||||
clearInterval(intervaloRelogio);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Funções para formatar os relógios
|
||||
function formatarRelogio(timestamp: number | null, ajusteGMT: number = 0): string {
|
||||
if (!timestamp) return '--:--:--';
|
||||
// Aplicar GMT offset ao timestamp
|
||||
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática
|
||||
// Quando GMT ≠ 0, aplicar offset configurado ao timestamp
|
||||
let timestampAjustado: number;
|
||||
if (ajusteGMT !== 0) {
|
||||
// Aplicar offset configurado
|
||||
timestampAjustado = timestamp + (ajusteGMT * 60 * 60 * 1000);
|
||||
} else {
|
||||
// Quando GMT = 0, manter timestamp UTC puro
|
||||
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
|
||||
timestampAjustado = timestamp;
|
||||
}
|
||||
const data = new Date(timestampAjustado);
|
||||
return data.toLocaleTimeString('pt-BR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
function formatarDataRelogio(timestamp: number | null, ajusteGMT: number = 0): string {
|
||||
if (!timestamp) return '--/--/----';
|
||||
// Aplicar GMT offset ao timestamp
|
||||
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleDateString() fazer a conversão automática
|
||||
// Quando GMT ≠ 0, aplicar offset configurado ao timestamp
|
||||
let timestampAjustado: number;
|
||||
if (ajusteGMT !== 0) {
|
||||
// Aplicar offset configurado
|
||||
timestampAjustado = timestamp + (ajusteGMT * 60 * 60 * 1000);
|
||||
} else {
|
||||
// Quando GMT = 0, manter timestamp UTC puro
|
||||
// O toLocaleDateString() converterá automaticamente para o timezone local do navegador
|
||||
timestampAjustado = timestamp;
|
||||
}
|
||||
const data = new Date(timestampAjustado);
|
||||
return data.toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
// Função para formatar timestamp ISO
|
||||
function formatarTimestampISO(timestamp: number | null): string {
|
||||
if (!timestamp) return '--';
|
||||
return new Date(timestamp).toISOString();
|
||||
}
|
||||
|
||||
// Função para obter nome do fuso horário baseado no offset
|
||||
function obterNomeFusoHorario(offset: number): string {
|
||||
const fusos: Record<number, string> = {
|
||||
'-12': 'IDLW (Linha Internacional de Data Oeste)',
|
||||
'-11': 'HST (Hawaii-Aleutian Standard Time)',
|
||||
'-10': 'HST (Hawaii Standard Time)',
|
||||
'-9': 'AKST (Alaska Standard Time)',
|
||||
'-8': 'PST (Pacific Standard Time)',
|
||||
'-7': 'MST (Mountain Standard Time)',
|
||||
'-6': 'CST (Central Standard Time)',
|
||||
'-5': 'EST (Eastern Standard Time)',
|
||||
'-4': 'AST (Atlantic Standard Time)',
|
||||
'-3': 'BRT (Brasília Time) / ART (Argentina Time)',
|
||||
'-2': 'GST (South Georgia Standard Time)',
|
||||
'-1': 'AZOT (Azores Standard Time)',
|
||||
'0': 'UTC / GMT (Greenwich Mean Time)',
|
||||
'1': 'CET (Central European Time)',
|
||||
'2': 'EET (Eastern European Time)',
|
||||
'3': 'MSK (Moscow Standard Time)',
|
||||
'4': 'GST (Gulf Standard Time)',
|
||||
'5': 'PKT (Pakistan Standard Time)',
|
||||
'6': 'BST (Bangladesh Standard Time)',
|
||||
'7': 'ICT (Indochina Time)',
|
||||
'8': 'CST (China Standard Time)',
|
||||
'9': 'JST (Japan Standard Time)',
|
||||
'10': 'AEST (Australian Eastern Standard Time)',
|
||||
'11': 'SBT (Solomon Islands Time)',
|
||||
'12': 'NZST (New Zealand Standard Time)',
|
||||
};
|
||||
return fusos[offset.toString()] || `UTC${offset >= 0 ? '+' : ''}${offset}`;
|
||||
}
|
||||
|
||||
// Função para calcular diferença em horas e minutos
|
||||
function calcularDiferencaFuso(offset: number): string {
|
||||
const horas = Math.abs(offset);
|
||||
const minutos = 0; // Offset em horas completas
|
||||
return `${horas}h ${minutos}m ${offset < 0 ? 'atrás' : 'à frente'} de UTC`;
|
||||
}
|
||||
|
||||
function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
setTimeout(() => {
|
||||
@@ -55,6 +214,9 @@
|
||||
});
|
||||
|
||||
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
||||
|
||||
// Recarregar configuração para atualizar status
|
||||
// A query será atualizada automaticamente pelo useQuery
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar configuração:', error);
|
||||
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao salvar configuração');
|
||||
@@ -69,10 +231,37 @@
|
||||
try {
|
||||
const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {});
|
||||
if (resultado.sucesso) {
|
||||
// Atualizar status de sincronização
|
||||
statusSincronizacao = {
|
||||
ultimaSincronizacao: Date.now(),
|
||||
offsetSegundos: resultado.offsetSegundos,
|
||||
usandoServidorExterno: resultado.usandoServidorExterno,
|
||||
};
|
||||
|
||||
// Atualizar timestamps dos relógios
|
||||
timestampUTC = resultado.timestamp;
|
||||
timestampOriginal = resultado.timestamp;
|
||||
|
||||
// Calcular horário atual com GMT offset
|
||||
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática
|
||||
let timestampAjustado: number;
|
||||
if (gmtOffset !== 0) {
|
||||
timestampAjustado = timestampUTC + (gmtOffset * 60 * 60 * 1000);
|
||||
} else {
|
||||
// Quando GMT = 0, manter timestamp UTC puro
|
||||
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
|
||||
timestampAjustado = timestampUTC;
|
||||
}
|
||||
const horarioAtual = new Date(timestampAjustado).toLocaleTimeString('pt-BR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
mostrarMensagem(
|
||||
'success',
|
||||
resultado.usandoServidorExterno
|
||||
? `Sincronização bem-sucedida! Offset: ${resultado.offsetSegundos}s`
|
||||
? `Sincronização bem-sucedida! Offset: ${resultado.offsetSegundos}s | Horário atual: ${horarioAtual}`
|
||||
: 'Usando relógio do PC (servidor externo não disponível)'
|
||||
);
|
||||
} else {
|
||||
@@ -85,6 +274,24 @@
|
||||
testando = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Função para atualizar relógios manualmente
|
||||
async function atualizarRelogios() {
|
||||
await obterTempoSincronizado();
|
||||
mostrarMensagem('success', 'Relógios atualizados!');
|
||||
}
|
||||
|
||||
function formatarDataHora(timestamp: number | null): string {
|
||||
if (!timestamp) return 'Nunca';
|
||||
return new Date(timestamp).toLocaleString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
||||
@@ -117,6 +324,134 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Relógios em Tempo Real -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title">Relógios Sincronizados</h2>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-info"
|
||||
onclick={atualizarRelogios}
|
||||
disabled={testando || processando}
|
||||
>
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
Atualizar Relógios
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/70 mb-6">
|
||||
Visualização em tempo real dos horários sincronizados. O primeiro relógio mostra o horário original (UTC) da fonte de sincronismo escolhida, e o segundo mostra o horário ajustado conforme o GMT configurado.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Relógio Original (UTC) -->
|
||||
<div class="bg-gradient-to-br from-blue-500/10 to-blue-600/10 rounded-xl p-6 border-2 border-blue-500/30">
|
||||
<div class="text-center">
|
||||
<div class="text-sm font-semibold text-blue-600 uppercase tracking-wide mb-2">
|
||||
Horário Original (UTC)
|
||||
</div>
|
||||
<div class="text-5xl font-bold text-blue-700 mb-2 font-mono">
|
||||
{formatarRelogio(timestampOriginal, 0)}
|
||||
</div>
|
||||
<div class="text-sm text-blue-600/80 mb-1">
|
||||
{formatarDataRelogio(timestampOriginal, 0)}
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider my-3 opacity-30"></div>
|
||||
|
||||
<!-- Detalhes do Fuso Horário -->
|
||||
<div class="text-left space-y-2 mt-4">
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-blue-700">Fuso Horário:</span>
|
||||
<span class="text-blue-600/90 ml-1">UTC / GMT (Greenwich Mean Time)</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-blue-700">Offset UTC:</span>
|
||||
<span class="text-blue-600/90 ml-1">±00:00 (Coordenado Universal)</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-blue-700">Fonte:</span>
|
||||
<span class="text-blue-600/90 ml-1">
|
||||
{statusSincronizacao?.usandoServidorExterno ? `Servidor NTP (${servidorNTP})` : 'Relógio do PC'}
|
||||
</span>
|
||||
</div>
|
||||
{#if statusSincronizacao?.offsetSegundos !== null && statusSincronizacao?.offsetSegundos !== undefined}
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-blue-700">Offset Calculado:</span>
|
||||
<span class="text-blue-600/90 ml-1">
|
||||
{statusSincronizacao.offsetSegundos > 0 ? '+' : ''}{statusSincronizacao.offsetSegundos}s
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if timestampOriginal}
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-blue-700">Timestamp UTC:</span>
|
||||
<span class="text-blue-600/90 ml-1 font-mono text-[10px]">
|
||||
{formatarTimestampISO(timestampOriginal)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-blue-700">Formato Recebido:</span>
|
||||
<span class="text-blue-600/90 ml-1">ISO 8601 (UTC)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relógio com GMT Ajustado -->
|
||||
<div class="bg-gradient-to-br from-primary/10 to-primary/20 rounded-xl p-6 border-2 border-primary/30">
|
||||
<div class="text-center">
|
||||
<div class="text-sm font-semibold text-primary uppercase tracking-wide mb-2">
|
||||
Horário com GMT {gmtOffset >= 0 ? '+' : ''}{gmtOffset}
|
||||
</div>
|
||||
<div class="text-5xl font-bold text-primary mb-2 font-mono">
|
||||
{formatarRelogio(timestampUTC, gmtOffset)}
|
||||
</div>
|
||||
<div class="text-sm text-primary/80 mb-1">
|
||||
{formatarDataRelogio(timestampUTC, gmtOffset)}
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider my-3 opacity-30"></div>
|
||||
|
||||
<!-- Detalhes do Fuso Horário Aplicado -->
|
||||
<div class="text-left space-y-2 mt-4">
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-primary">Fuso Horário:</span>
|
||||
<span class="text-primary/90 ml-1">{obterNomeFusoHorario(gmtOffset)}</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-primary">Offset Configurado:</span>
|
||||
<span class="text-primary/90 ml-1">
|
||||
GMT{gmtOffset >= 0 ? '+' : ''}{gmtOffset} ({calcularDiferencaFuso(gmtOffset)})
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-primary">Ajuste Aplicado:</span>
|
||||
<span class="text-primary/90 ml-1">
|
||||
{gmtOffset >= 0 ? '+' : ''}{gmtOffset}:00 UTC
|
||||
</span>
|
||||
</div>
|
||||
{#if timestampUTC}
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-primary">Timestamp Local:</span>
|
||||
<span class="text-primary/90 ml-1 font-mono text-[10px]">
|
||||
{formatarTimestampISO(gmtOffset !== 0 ? timestampUTC + (gmtOffset * 60 * 60 * 1000) : timestampUTC)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-primary">Status:</span>
|
||||
<span class="text-primary/90 ml-1">Ajuste aplicado em tempo real</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulário -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
@@ -226,6 +561,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status de Sincronização -->
|
||||
{#if statusSincronizacao}
|
||||
<div class="divider"></div>
|
||||
<h2 class="card-title mb-4">Status de Sincronização</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="stat bg-base-200 rounded-lg p-4">
|
||||
<div class="stat-title text-xs">Última Sincronização</div>
|
||||
<div class="stat-value text-lg">
|
||||
{formatarDataHora(statusSincronizacao.ultimaSincronizacao)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-lg p-4">
|
||||
<div class="stat-title text-xs">Offset Calculado</div>
|
||||
<div class="stat-value text-lg">
|
||||
{statusSincronizacao.offsetSegundos !== null
|
||||
? `${statusSincronizacao.offsetSegundos > 0 ? '+' : ''}${statusSincronizacao.offsetSegundos}s`
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-lg p-4">
|
||||
<div class="stat-title text-xs">Fonte de Tempo</div>
|
||||
<div class="stat-value text-lg">
|
||||
{statusSincronizacao.usandoServidorExterno ? 'Servidor NTP' : 'Relógio do PC'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="card-actions justify-end mt-6 gap-3">
|
||||
{#if usarServidorExterno}
|
||||
@@ -272,7 +635,15 @@
|
||||
específica.
|
||||
</p>
|
||||
<p class="text-sm mt-1">
|
||||
Servidores NTP recomendados: pool.ntp.org, time.google.com, time.windows.com
|
||||
Servidores NTP recomendados: pool.ntp.org, time.google.com, time.windows.com, ntp.br
|
||||
</p>
|
||||
<p class="text-sm mt-2">
|
||||
<strong>Como funciona:</strong> O servidor NTP configurado é mapeado para uma API HTTP que retorna UTC.
|
||||
O GMT offset configurado é então aplicado no frontend para exibir o horário correto.
|
||||
</p>
|
||||
<p class="text-sm mt-1">
|
||||
<strong>Importante:</strong> Todos os servidores NTP retornam tempo em UTC. O GMT offset é aplicado
|
||||
apenas uma vez no frontend para ajustar ao fuso horário local (ex: GMT-3 para Brasília).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user