feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -62,7 +62,11 @@ export interface InformacoesDispositivo {
/**
* Detecta informações do navegador
*/
function detectarNavegador(): { browser: string; browserVersion: string; engine: string } {
function detectarNavegador(): {
browser: string;
browserVersion: string;
engine: string;
} {
if (typeof navigator === 'undefined') {
return { browser: 'Desconhecido', browserVersion: '', engine: '' };
}
@@ -123,7 +127,7 @@ function detectarSistemaOperacional(): {
sistemaOperacional: 'Desconhecido',
osVersion: '',
arquitetura: '',
plataforma: '',
plataforma: ''
};
}
@@ -144,7 +148,7 @@ function detectarSistemaOperacional(): {
'10.0': '10/11',
'6.3': '8.1',
'6.2': '8',
'6.1': '7',
'6.1': '7'
};
osVersion = versions[version] || version;
}
@@ -191,7 +195,7 @@ function detectarTipoDispositivo(): {
deviceType: 'Desconhecido',
isMobile: false,
isTablet: false,
isDesktop: true,
isDesktop: true
};
}
@@ -213,7 +217,10 @@ function detectarTipoDispositivo(): {
/**
* Obtém informações da tela
*/
function obterInformacoesTela(): { screenResolution: string; coresTela: number } {
function obterInformacoesTela(): {
screenResolution: string;
coresTela: number;
} {
if (typeof screen === 'undefined') {
return { screenResolution: 'Desconhecido', coresTela: 0 };
}
@@ -232,7 +239,8 @@ async function obterInformacoesConexao(): Promise<string> {
return 'Desconhecido';
}
const connection = (navigator as unknown as { connection?: { effectiveType?: string } }).connection;
const connection = (navigator as unknown as { connection?: { effectiveType?: string } })
.connection;
if (connection?.effectiveType) {
return connection.effectiveType;
}
@@ -260,12 +268,7 @@ function obterInformacoesMemoria(): string {
* Calcula distância entre duas coordenadas (fórmula de Haversine)
* Retorna distância em metros
*/
function calcularDistancia(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
function calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000; // Raio da Terra em metros
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
@@ -287,7 +290,7 @@ function obterTimezonePorCoordenadas(latitude: number, longitude: number): strin
if (longitude >= -45 && longitude <= -30 && latitude >= -10 && latitude <= 5) {
return 'America/Recife'; // UTC-3
}
// Fallback: usar timezone do sistema
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -358,7 +361,7 @@ async function capturarLocalizacaoUnica(
// Calcular confiabilidade: cada sinal adiciona pontos
let pontos = 0;
const maxPontos = 7;
if (sinaisGPSReal.temAltitude) pontos += 1;
if (sinaisGPSReal.temAltitudeAccuracy) pontos += 1;
if (sinaisGPSReal.temHeading) pontos += 0.5;
@@ -412,7 +415,11 @@ async function obterLocalizacaoMultipla(): Promise<{
motivoSuspeita?: string;
}> {
if (typeof navigator === 'undefined' || !navigator.geolocation) {
return { confiabilidade: 0, suspeitaSpoofing: true, motivoSuspeita: 'Geolocalização não suportada' };
return {
confiabilidade: 0,
suspeitaSpoofing: true,
motivoSuspeita: 'Geolocalização não suportada'
};
}
// Capturar 3 leituras com intervalo de 2 segundos entre elas
@@ -426,7 +433,7 @@ async function obterLocalizacaoMultipla(): Promise<{
for (let i = 0; i < 3; i++) {
const leitura = await capturarLocalizacaoUnica(true, 8000);
if (leitura.latitude && leitura.longitude && leitura.confiabilidade > 0) {
leituras.push({
lat: leitura.latitude,
@@ -436,7 +443,7 @@ async function obterLocalizacaoMultipla(): Promise<{
confiabilidade: leitura.confiabilidade
});
}
// Aguardar 2 segundos entre leituras (exceto na última)
if (i < 2) {
await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -444,7 +451,11 @@ async function obterLocalizacaoMultipla(): Promise<{
}
if (leituras.length === 0) {
return { confiabilidade: 0, suspeitaSpoofing: true, motivoSuspeita: 'Não foi possível obter localização' };
return {
confiabilidade: 0,
suspeitaSpoofing: true,
motivoSuspeita: 'Não foi possível obter localização'
};
}
// Se tivermos menos de 2 leituras, usar única leitura com baixa confiança
@@ -546,7 +557,7 @@ export async function obterLocalizacaoRapida(): Promise<{
try {
// Uma única leitura rápida com timeout curto
const leitura = await capturarLocalizacaoUnica(true, 3000); // 3 segundos máximo
if (!leitura.latitude || !leitura.longitude || leitura.confiabilidade === 0) {
return {};
}
@@ -566,12 +577,12 @@ export async function obterLocalizacaoRapida(): Promise<{
}
}
);
const geocodeTimeout = new Promise<Response>((_, reject) =>
const geocodeTimeout = new Promise<Response>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 2000)
);
const response = await Promise.race([geocodePromise, geocodeTimeout]);
if (response.ok) {
const data = (await response.json()) as {
address?: {
@@ -649,7 +660,18 @@ export async function obterLocalizacao(): Promise<{
};
}
const { latitude, longitude, precisao, altitude, altitudeAccuracy, heading, speed, confiabilidade, suspeitaSpoofing, motivoSuspeita } = localizacaoMultipla;
const {
latitude,
longitude,
precisao,
altitude,
altitudeAccuracy,
heading,
speed,
confiabilidade,
suspeitaSpoofing,
motivoSuspeita
} = localizacaoMultipla;
// Tentar obter endereço via reverse geocoding
let endereco = '';
@@ -695,9 +717,13 @@ export async function obterLocalizacao(): Promise<{
if (typeof navigator !== 'undefined') {
const timezoneAtual = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timezoneEsperado = obterTimezonePorCoordenadas(latitude, longitude);
// Se timezone é muito diferente, pode ser suspeito
if (timezoneAtual !== timezoneEsperado && timezoneAtual !== 'America/Recife' && timezoneEsperado !== 'America/Recife') {
if (
timezoneAtual !== timezoneEsperado &&
timezoneAtual !== 'America/Recife' &&
timezoneEsperado !== 'America/Recife'
) {
console.warn(`Timezone inconsistente: esperado ${timezoneEsperado}, atual ${timezoneAtual}`);
}
}
@@ -748,13 +774,22 @@ export async function obterIPPublico(): Promise<string | undefined> {
* Solicita permissão para acesso aos sensores de movimento (iOS 13+)
*/
async function solicitarPermissaoSensor(): Promise<PermissionState> {
if (typeof DeviceMotionEvent === 'undefined' || typeof (DeviceMotionEvent as { requestPermission?: () => Promise<PermissionState> }).requestPermission !== 'function') {
if (
typeof DeviceMotionEvent === 'undefined' ||
typeof (
DeviceMotionEvent as {
requestPermission?: () => Promise<PermissionState>;
}
).requestPermission !== 'function'
) {
// Permissão não necessária ou já concedida (navegadores modernos)
return 'granted';
}
try {
const requestPermission = (DeviceMotionEvent as { requestPermission: () => Promise<PermissionState> }).requestPermission;
const requestPermission = (
DeviceMotionEvent as { requestPermission: () => Promise<PermissionState> }
).requestPermission;
const resultado = await requestPermission();
return resultado;
} catch (error) {
@@ -783,7 +818,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
// Solicitar permissão (especialmente necessário no iOS 13+)
const permissao = await solicitarPermissaoSensor();
if (permissao === 'denied') {
return {
sensorDisponivel: true,
@@ -792,37 +827,50 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
}
return new Promise((resolve) => {
const leiturasAcelerometro: Array<{ x: number; y: number; z: number; timestamp: number }> = [];
const leiturasGiroscopio: Array<{ alpha: number; beta: number; gamma: number; timestamp: number }> = [];
const leiturasAcelerometro: Array<{
x: number;
y: number;
z: number;
timestamp: number;
}> = [];
const leiturasGiroscopio: Array<{
alpha: number;
beta: number;
gamma: number;
timestamp: number;
}> = [];
const timeoutId = setTimeout(() => {
window.removeEventListener('devicemotion', handleDeviceMotion);
window.removeEventListener('deviceorientation', handleDeviceOrientation);
// Processar dados de acelerômetro
let acelerometro: DadosAcelerometro | undefined;
if (leiturasAcelerometro.length > 0) {
const ultimaLeitura = leiturasAcelerometro[leiturasAcelerometro.length - 1]!;
// Calcular magnitude média
const magnitudes = leiturasAcelerometro.map(l =>
const magnitudes = leiturasAcelerometro.map((l) =>
Math.sqrt(l.x * l.x + l.y * l.y + l.z * l.z)
);
const magnitude = magnitudes.reduce((sum, m) => sum + m, 0) / magnitudes.length;
// Calcular variância para detectar movimento
const mediaX = leiturasAcelerometro.reduce((sum, l) => sum + l.x, 0) / leiturasAcelerometro.length;
const mediaY = leiturasAcelerometro.reduce((sum, l) => sum + l.y, 0) / leiturasAcelerometro.length;
const mediaZ = leiturasAcelerometro.reduce((sum, l) => sum + l.z, 0) / leiturasAcelerometro.length;
const variacoes = leiturasAcelerometro.map(l =>
Math.pow(l.x - mediaX, 2) + Math.pow(l.y - mediaY, 2) + Math.pow(l.z - mediaZ, 2)
const mediaX =
leiturasAcelerometro.reduce((sum, l) => sum + l.x, 0) / leiturasAcelerometro.length;
const mediaY =
leiturasAcelerometro.reduce((sum, l) => sum + l.y, 0) / leiturasAcelerometro.length;
const mediaZ =
leiturasAcelerometro.reduce((sum, l) => sum + l.z, 0) / leiturasAcelerometro.length;
const variacoes = leiturasAcelerometro.map(
(l) => (l.x - mediaX) ** 2 + (l.y - mediaY) ** 2 + (l.z - mediaZ) ** 2
);
const variacao = variacoes.reduce((sum, v) => sum + v, 0) / variacoes.length;
// Detectar movimento: se variância > 0.01, há movimento
const movimentoDetectado = variacao > 0.01;
acelerometro = {
x: ultimaLeitura.x,
y: ultimaLeitura.y,
@@ -833,7 +881,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
timestamp: ultimaLeitura.timestamp
};
}
// Processar dados de giroscópio
let giroscopio: DadosGiroscopio | undefined;
if (leiturasGiroscopio.length > 0) {
@@ -844,7 +892,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
gamma: ultimaLeitura.gamma || 0
};
}
resolve({
acelerometro,
giroscopio,
@@ -852,7 +900,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
permissaoNegada: false
});
}, duracaoMs);
function handleDeviceMotion(event: DeviceMotionEvent) {
if (event.accelerationIncludingGravity) {
const acc = event.accelerationIncludingGravity;
@@ -866,7 +914,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
}
}
}
function handleDeviceOrientation(event: DeviceOrientationEvent) {
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
leiturasGiroscopio.push({
@@ -877,7 +925,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
});
}
}
window.addEventListener('devicemotion', handleDeviceMotion);
window.addEventListener('deviceorientation', handleDeviceOrientation);
});
@@ -922,14 +970,15 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
informacoes.coresTela = tela.coresTela;
// Informações de conexão, memória e localização (assíncronas)
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao, dadosSensores] = await Promise.all([
obterInformacoesConexao(),
Promise.resolve(obterInformacoesMemoria()),
obterIPPublico(),
getLocalIP(),
obterLocalizacao(),
obterDadosAcelerometro(5000), // Coletar dados por 5 segundos
]);
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao, dadosSensores] =
await Promise.all([
obterInformacoesConexao(),
Promise.resolve(obterInformacoesMemoria()),
obterIPPublico(),
getLocalIP(),
obterLocalizacao(),
obterDadosAcelerometro(5000) // Coletar dados por 5 segundos
]);
informacoes.connectionType = connectionType;
informacoes.memoryInfo = memoryInfo;
@@ -961,4 +1010,3 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
return informacoes;
}