tech stats + db dump
Some checks failed
Frontend Build and Deploy / build (push) Failing after 57s

This commit is contained in:
José Henrique 2025-06-19 21:40:38 -03:00
parent ceaffc088e
commit 46e76acad3
5 changed files with 197 additions and 10 deletions

View File

@ -144,4 +144,23 @@ export interface OpenCandDataAvailabilityStats {
receitaCandidatos: number[]; receitaCandidatos: number[];
redeSocialCandidatos: number[]; redeSocialCandidatos: number[];
fotosCandidatos: number[]; fotosCandidatos: number[];
}
export interface OpenCandDatabaseStats {
tables: {
name: string;
totalSize: number; // in bytes
entries: number; // number of rows
}[];
materializedViews: {
name: string;
totalSize: number; // in bytes
entries: number; // number of rows
}[];
indexes: {
amount: number; // number of indexes
size: number; // total size of indexes in bytes
};
totalSize: number; // total size of the database in bytes
totalEntries: number; // total number of entries across all tables
} }

View File

@ -1,6 +1,6 @@
import { BaseApiClient } from './base'; import { BaseApiClient } from './base';
import { API_CONFIG } from '../config/api'; import { API_CONFIG } from '../config/api';
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, PlatformStats, RandomCandidate } from './apiModels'; import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, OpenCandDatabaseStats, PlatformStats, RandomCandidate } from './apiModels';
import type { EnrichmentResponse, StatisticsConfig, ValueSumRequest, ValueSumResponse } from './apiStatisticsModels'; import type { EnrichmentResponse, StatisticsConfig, ValueSumRequest, ValueSumResponse } from './apiStatisticsModels';
/** /**
@ -27,6 +27,13 @@ export class OpenCandApi extends BaseApiClient {
return this.get<OpenCandDataAvailabilityStats>('/v1/stats/data-availability'); return this.get<OpenCandDataAvailabilityStats>('/v1/stats/data-availability');
} }
/**
* Get the database tech stats
*/
async getDatabaseTechStats(): Promise<OpenCandDatabaseStats> {
return this.get<OpenCandDatabaseStats>(`/v1/stats/tech`);
}
/** /**
* Search for candidates by name or other attributes * Search for candidates by name or other attributes
*/ */

View File

@ -1,13 +1,17 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { FaCloudDownloadAlt, FaGoogleDrive } from 'react-icons/fa';
import { openCandApi } from '../api'; import { openCandApi } from '../api';
import type { OpenCandDataAvailabilityStats } from '../api/apiModels'; import type { OpenCandDataAvailabilityStats, OpenCandDatabaseStats } from '../api/apiModels';
import Card from '../shared/Card'; import Card from '../shared/Card';
import ErrorPage from './ErrorPage'; import ErrorPage from './ErrorPage';
const DataStatsPage: React.FC = () => { const DataStatsPage: React.FC = () => {
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null); const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
const [dbStats, setDbStats] = useState<OpenCandDatabaseStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [dbLoading, setDbLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [dbError, setDbError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchStats = async () => { const fetchStats = async () => {
@ -22,8 +26,20 @@ const DataStatsPage: React.FC = () => {
setLoading(false); setLoading(false);
} }
}; };
const fetchDbStats = async () => {
try {
setDbLoading(true);
const dbData = await openCandApi.getDatabaseTechStats();
setDbStats(dbData);
} catch (err) {
setDbError('Erro ao carregar estatísticas técnicas do banco de dados');
console.error('Error fetching database tech stats:', err);
} finally {
setDbLoading(false);
}
};
fetchStats(); fetchStats();
fetchDbStats();
}, []); }, []);
if (loading) { if (loading) {
@ -68,14 +84,14 @@ const DataStatsPage: React.FC = () => {
{ key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' }, { key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
{ key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' }, { key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
{ key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' }, { key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos', icon: '📸' }, { key: 'fotosCandidatos', label: 'Fotos de Candidatos (API)', icon: '📸' },
]; ];
return ( return (
<div className="min-h-screen py-20 px-4 hover:cursor-default"> <div className="min-h-screen py-20 px-4 hover:cursor-default">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
{/* Header */} {/* Header */}
<div className="text-center mb-12 animate-fade-in"> <div className="text-center mb-12 animate-slide-in-left">
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white"> <h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
Disponibilidade de Dados Disponibilidade de Dados
</h1> </h1>
@ -85,6 +101,23 @@ const DataStatsPage: React.FC = () => {
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div> <div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
</div> </div>
{/* Download DB Dump Section */}
<div className="flex justify-center mb-10 animate-slide-in-left backdrop-blur-xs bg-gray-800/10 rounded-xl shadow-lg hover:shadow-xl transform transition-all duration-200 overflow-hidden ring-1 ring-gray-700 hover:ring-indigo-300 px-8 py-6 flex flex-col md:flex-row items-center gap-4 max-w-xl-auto">
<div className="flex items-center gap-3 mb-2 md:mb-0">
<FaCloudDownloadAlt className="text-3xl text-green-400" />
<span className="text-lg font-semibold text-white">Download do Dump do Banco de Dados</span>
</div>
<a
href="https://drive.google.com/file/d/1cfMItrsAdv8y8YUNp04D33s6pYrRbmDn/view?usp=sharing"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 py-2 px-5 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-600 hover:to-purple-700 transition-all duration-300 transform shadow-lg hover:shadow-xl"
>
<FaGoogleDrive className="text-xl" />
Google Drive
</a>
</div>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl"> <Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
@ -133,7 +166,7 @@ const DataStatsPage: React.FC = () => {
{sortedYears.map((year, index) => ( {sortedYears.map((year, index) => (
<th <th
key={year} key={year}
className="text-center p-4 text-white font-semibold border-b border-gray-700/30 animate-fade-in" className="text-center p-4 text-white font-semibold border-b border-gray-700/30 animate-slide-in-left"
style={{ animationDelay: `${index * 50}ms` }} style={{ animationDelay: `${index * 50}ms` }}
> >
{year} {year}
@ -178,6 +211,131 @@ const DataStatsPage: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Database Tech Stats Section */}
<div className="mt-20 flex justify-center">
<div className="bg-gray-800/10 backdrop-blur-xs rounded-xl shadow-lg hover:shadow-xl transform hover:scale-[1.005] transition-all duration-200 overflow-hidden ring-1 ring-gray-700 hover:ring-indigo-300 mb-12 w-full max-w-2xl">
<div className="p-6 border-b border-gray-700/30 bg-gray-800/10">
<h2 className="text-2xl font-bold text-white">Dados Técnicas do Banco de Dados</h2>
<p className="text-gray-400 mt-2">Informações sobre tabelas, views materializadas e índices</p>
</div>
{dbLoading ? (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-indigo-600 border-t-transparent mr-4"></div>
<span className="text-gray-300">Carregando dados do banco de dados...</span>
</div>
) : dbError ? (
<div className="p-8 text-red-400">{dbError}</div>
) : dbStats ? (
<div className="p-6 space-y-10">
{/* Tables */}
<div>
<h3 className="text-xl font-semibold text-white mb-2">Tabelas</h3>
<div className="overflow-x-auto max-h-112" style={{maxHeight: '32rem'}}>
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-gray-800/10">
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Nome</th>
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Entradas</th>
</tr>
</thead>
<tbody>
{dbStats.tables.map((table) => {
const name = table.name.replace(/^public\./, '');
const sizeMB = table.totalSize / 1024 / 1024;
const sizeDisplay = sizeMB > 1024
? `${(sizeMB / 1024).toFixed(2)} GB`
: `${sizeMB.toFixed(2)} MB`;
return (
<tr key={table.name} className="hover:bg-gray-800/10 transition-all duration-200">
<td className="p-3 text-white">{name}</td>
<td className="p-3 text-gray-300">{sizeDisplay}</td>
<td className="p-3 text-gray-300">{table.entries.toLocaleString()}</td>
</tr>
);
})}
{/* Total row */}
{(() => {
const totalSize = dbStats.tables.reduce((acc, t) => acc + t.totalSize, 0);
const totalEntries = dbStats.tables.reduce((acc, t) => acc + t.entries, 0);
const totalMB = totalSize / 1024 / 1024;
const totalDisplay = totalMB > 1024
? `${(totalMB / 1024).toFixed(2)} GB`
: `${totalMB.toFixed(2)} MB`;
return (
<tr className="font-bold bg-gray-900/30">
<td className="p-3 text-white">Total</td>
<td className="p-3 text-gray-300">{totalDisplay}</td>
<td className="p-3 text-gray-300">{totalEntries.toLocaleString()}</td>
</tr>
);
})()}
</tbody>
</table>
</div>
</div>
{/* Materialized Views */}
<div>
<h3 className="text-xl font-semibold text-white mb-2">Views Materializadas</h3>
<div className="overflow-x-auto max-h-96" style={{maxHeight: '28rem'}}>
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-gray-800/10">
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Nome</th>
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Entradas</th>
</tr>
</thead>
<tbody>
{dbStats.materializedViews.map((view) => {
const name = view.name.replace(/^public\./, '');
const sizeMB = view.totalSize / 1024 / 1024;
const sizeDisplay = sizeMB > 1024
? `${(sizeMB / 1024).toFixed(2)} GB`
: `${sizeMB.toFixed(2)} MB`;
return (
<tr key={view.name} className="hover:bg-gray-800/10 transition-all duration-200">
<td className="p-3 text-white">{name}</td>
<td className="p-3 text-gray-300">{sizeDisplay}</td>
<td className="p-3 text-gray-300">{view.entries.toLocaleString()}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Indexes */}
<div>
<h3 className="text-xl font-semibold text-white mb-2">Índices</h3>
<div className="overflow-x-auto max-h-96 flex justify-center" style={{maxHeight: '28rem'}}>
<table className="w-auto text-left border-collapse mx-auto">
<thead>
<tr className="bg-gray-800/10">
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Quantidade</th>
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 text-gray-300">{dbStats.indexes.amount}</td>
{(() => {
const sizeMB = dbStats.indexes.size / 1024 / 1024;
const sizeDisplay = sizeMB > 1024
? `${(sizeMB / 1024).toFixed(2)} GB`
: `${sizeMB.toFixed(2)} MB`;
return <td className="p-3 text-gray-300">{sizeDisplay}</td>;
})()}
</tr>
</tbody>
</table>
</div>
</div>
</div>
) : null}
</div>
</div>
</div> </div>
); );
}; };

View File

@ -95,14 +95,14 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-gray-600">Patrimônio Inicial (<span className="text-xs text-gray-500">{enrichmentData.anoInicial}</span>):</span> <span className="text-gray-600">Patrimônio Inicial (<span className="text-xs text-gray-500">{enrichmentData.anoInicial}</span>):</span>
<span className="text-gray-900 font-medium"> <span className="text-gray-900 font-medium">
R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR') || '0'} R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-gray-600">Patrimônio Final (<span className="text-xs text-gray-500">{enrichmentData.anoFinal}</span>):</span> <span className="text-gray-600">Patrimônio Final (<span className="text-xs text-gray-500">{enrichmentData.anoFinal}</span>):</span>
<span className="text-gray-900 font-medium"> <span className="text-gray-900 font-medium">
R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR') || '0'} R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
</span> </span>
</div> </div>
<div className="border-t border-gray-200 pt-3"> <div className="border-t border-gray-200 pt-3">
@ -112,7 +112,7 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
enrichmentData.enriquecimento >= 0 ? 'text-green-600' : 'text-red-600' enrichmentData.enriquecimento >= 0 ? 'text-green-600' : 'text-red-600'
}`}> }`}>
{enrichmentData.enriquecimento >= 0 ? '+' : ''} {enrichmentData.enriquecimento >= 0 ? '+' : ''}
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR') || '0'} R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
</span> </span>
</div> </div>
</div> </div>
@ -202,7 +202,7 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
)} )}
<td className="py-3 text-gray-900">{item.ano || 'N/A'}</td> <td className="py-3 text-gray-900">{item.ano || 'N/A'}</td>
<td className="py-3 text-right font-medium text-gray-900"> <td className="py-3 text-right font-medium text-gray-900">
R$ {item.valor?.toLocaleString('pt-BR') || '0'} R$ {item.valor?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
</td> </td>
</tr> </tr>
))} ))}

View File

@ -66,6 +66,9 @@ const StatisticsPage: React.FC = () => {
<p className="text-white/70 text-lg"> <p className="text-white/70 text-lg">
Análise de dados e estatísticas dos candidatos e partidos brasileiros Análise de dados e estatísticas dos candidatos e partidos brasileiros
</p> </p>
<p className="text-white/70 text-lg">
Para mais informações, acesse a <a href="https://sig.tse.jus.br/ords/dwapr/r/seai/sig-eleicao/home" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">página de estatísticas do TSE</a> ou o <a href="https://divulgacandcontas.tse.jus.br/divulga/#/home" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">DivulgaCand do TSE</a>.
</p>
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div> <div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
</div> </div>