Initial commit: project structure and script setup
This commit is contained in:
commit
d01ae7ec46
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE/Editor folders
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Arquivos gerados pelo script
|
||||||
|
*.html
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Painel de Atividade de Usuários NGINX
|
||||||
|
|
||||||
|
Este projeto contém um script Python avançado para processar logs de acesso do NGINX (em formato CSV), analisar a atividade de usuários autenticados e gerar um painel de visualização interativo em um único arquivo HTML.
|
||||||
|
|
||||||
|
A ferramenta foi projetada para extrair insights sobre como e quando os usuários interagem com um sistema web (como o Nextcloud), categorizando suas ações e apresentando os dados em gráficos claros e detalhados.
|
||||||
|
|
||||||
|

|
||||||
|
*(Recomendação: Substitua esta imagem por um screenshot do relatório `painel_de_atividade_final.html`)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funcionalidades Principais
|
||||||
|
|
||||||
|
* **Processamento de Logs:** Lê arquivos de log NGINX em formato CSV, otimizado para lidar com grandes volumes de dados.
|
||||||
|
* **Inferência de Ações:** Analisa `User-Agent`, método HTTP (`GET`, `POST`, `PUT`, etc.) e URI para categorizar a atividade do usuário em três níveis:
|
||||||
|
* **Ativo:** Ações diretas com arquivos (edição, upload, download, exclusão).
|
||||||
|
* **Online:** Atividade geral de navegação e checagens em segundo plano.
|
||||||
|
* **Outros:** Sincronizações, interações com formulários e requisições não classificadas.
|
||||||
|
* **Visualizações Detalhadas:** Gera múltiplos gráficos para análise:
|
||||||
|
* **Nuvem de Atividade:** Um gráfico de dispersão que mostra a intensidade das atividades ao longo do tempo.
|
||||||
|
* **Heatmap Agregado:** Mostra os padrões de atividade de todos os usuários por dia da semana e hora.
|
||||||
|
* **Heatmap por Colaborador:** Gera um mapa de calor individual para cada usuário.
|
||||||
|
* **Timeline Diária:** Gráficos de linha suavizados que detalham a intensidade de cada categoria de ação por hora para um usuário e dia específicos.
|
||||||
|
* **Painel Interativo:** Produz um único arquivo `HTML` com filtros que permitem selecionar um colaborador específico e navegar entre sua visão agregada e a análise diária.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como Funciona
|
||||||
|
|
||||||
|
O script opera em quatro etapas principais:
|
||||||
|
|
||||||
|
1. **Leitura e Parsing:** Cada linha do arquivo de log CSV é lida. Uma função de parsing extrai informações cruciais como timestamp, username (extraído de cookies como `nc_username`), `User-Agent` e a requisição HTTP.
|
||||||
|
2. **Classificação de Ações:** A função `get_action_details` atua como o cérebro da análise. Ela usa uma série de regras para determinar a ação do usuário e o tipo de cliente (Web ou App de sincronização) com base nos dados extraídos.
|
||||||
|
3. **Visualização de Dados:** Utilizando as bibliotecas `pandas` para manipulação de dados e `matplotlib`/`seaborn` para a geração de gráficos, o script cria as visualizações. Cada gráfico é salvo em memória como uma imagem.
|
||||||
|
4. **Geração do Relatório:** As imagens dos gráficos são codificadas em Base64 e embutidas diretamente em um template HTML. Este template também inclui JavaScript para criar os menus de filtro interativos, resultando em um arquivo `painel_de_atividade_final.html` totalmente funcional e portátil.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
* **Python 3.8+**
|
||||||
|
* **Logs do NGINX em formato CSV:** O script espera um formato específico. Veja um exemplo de diretiva `log_format` para NGINX que gera a saída compatível:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
log_format custom_csv escape=json
|
||||||
|
'$time_iso8601,'
|
||||||
|
'$remote_addr,'
|
||||||
|
'"$http_user_agent",'
|
||||||
|
'"$http_cookie",'
|
||||||
|
'$status,'
|
||||||
|
'$body_bytes_sent,'
|
||||||
|
'$remote_user,'
|
||||||
|
'$request_time,'
|
||||||
|
'$upstream_response_time,'
|
||||||
|
'"$request"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/seu-site.access.log custom_csv;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como Utilizar
|
||||||
|
|
||||||
|
1. **Clone o Repositório**
|
||||||
|
```bash
|
||||||
|
git clone [URL_DO_SEU_REPO_GITEA]
|
||||||
|
cd relatorio-atividade-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Crie e Ative um Ambiente Virtual (Recomendado)**
|
||||||
|
```bash
|
||||||
|
# Para Linux/macOS
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Para Windows
|
||||||
|
python -m venv venv
|
||||||
|
.\venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Instale as Dependências**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Execute o Script**
|
||||||
|
O script precisa do caminho para o seu arquivo de log como argumento.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python src/process_log.py /caminho/para/seu/nginx_logs.csv
|
||||||
|
```
|
||||||
|
*Substitua `/caminho/para/seu/nginx_logs.csv` pelo caminho real do seu arquivo.*
|
||||||
|
|
||||||
|
5. **Abra o Relatório**
|
||||||
|
Após a execução, um arquivo chamado `painel_de_atividade_final.html` será criado no diretório raiz do projeto. Abra-o em qualquer navegador web para explorar o painel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customização
|
||||||
|
|
||||||
|
* **Categorias de Ação:** Para ajustar as categorias ou adicionar novas, modifique o dicionário `ACTION_CATEGORIES` no início do script `src/process_log.py`.
|
||||||
|
* **Cores dos Gráficos:** As cores para cada categoria podem ser alteradas no dicionário `CATEGORY_COLORS`.
|
||||||
|
* **Lógica de Inferência:** A função `get_action_details` pode ser expandida com mais regras para identificar atividades específicas do seu sistema.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
Este projeto está licenciado sob a Licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
pandas
|
||||||
|
matplotlib
|
||||||
|
seaborn
|
||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
# process_log.py
|
||||||
|
# Autor: Gemini (assistente de IA)
|
||||||
|
# Data: 26 de setembro de 2025, 22:08
|
||||||
|
# Descrição: Script para processamento de logs NGINX.
|
||||||
|
# Versão 14.1 (Correção Final):
|
||||||
|
# - Removido filtro 'Tipo de Cliente'. Agora exibe os gráficos Web e App simultaneamente.
|
||||||
|
# - Corrigido o cabeçalho do relatório para remover permanentemente o caminho do arquivo.
|
||||||
|
# - Lógica de geração de gráficos mantida, apenas a exibição no HTML foi corrigida.
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import seaborn as sns
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import numpy as np
|
||||||
|
from scipy.interpolate import make_interp_spline
|
||||||
|
|
||||||
|
# --- CONFIGURAÇÃO E MAPEAMENTOS ---
|
||||||
|
DAY_MAP_PT = {'Monday': 'Segunda-feira', 'Tuesday': 'Terça-feira', 'Wednesday': 'Quarta-feira',
|
||||||
|
'Thursday': 'Quinta-feira', 'Friday': 'Sexta-feira', 'Saturday': 'Sábado', 'Sunday': 'Domingo'}
|
||||||
|
|
||||||
|
ACTION_CATEGORIES = {
|
||||||
|
'Ativo': ['Edição de Documento', 'Sincronização (Upload/Download)', 'Upload (Web/Outros)',
|
||||||
|
'Download/Visualização (Web)', 'Criação de Pasta', 'Exclusão de Arquivo/Pasta', 'Mover/Renomear Arquivo'],
|
||||||
|
'Online': ['Navegação', 'Atividade em Segundo Plano (Web)'],
|
||||||
|
'Outros': ['Sincronização (Verificação)', 'Interação Web (Formulários, etc.)', 'Requisição Inválida']
|
||||||
|
}
|
||||||
|
CATEGORY_COLORS = {"Ativo": "#3498db", "Online": "#2ecc71", "Outros": "#f1c40f"}
|
||||||
|
DETAILED_LEGEND_HTML = f"""
|
||||||
|
<div style='font-size:0.9em; color:#333; margin-top:15px; border-top:1px solid #eee; padding-top:10px;'>
|
||||||
|
<p><strong>Legenda das Categorias:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong style='color:{CATEGORY_COLORS['Ativo']};'>Ativo:</strong> Ações que indicam trabalho direto com arquivos (uploads, downloads, edições, etc.).</li>
|
||||||
|
<li><strong style='color:{CATEGORY_COLORS['Online']};'>Online:</strong> Atividade geral de navegação e checagens automáticas da interface web.</li>
|
||||||
|
<li><strong style='color:{CATEGORY_COLORS['Outros']};'>Outros:</strong> Ações de verificação do app de sincronização, formulários e outras interações não classificadas.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- FUNÇÕES DE ANÁLISE E INFERÊNCIA ---
|
||||||
|
|
||||||
|
def get_action_details(req_method: str, uri: str, user_agent: str) -> tuple[str, str, str]:
|
||||||
|
ua = user_agent.lower()
|
||||||
|
action, client_type = 'Navegação', 'Web'
|
||||||
|
if 'mirall' in ua or 'nextcloud-desktop' in ua:
|
||||||
|
client_type = 'App'; action = 'Sincronização (Verificação)'
|
||||||
|
if req_method in ['PUT', 'GET']: action = 'Sincronização (Upload/Download)'
|
||||||
|
elif 'nextcloud-android' in ua or 'nextcloud-ios' in ua: client_type = 'App'
|
||||||
|
elif req_method == 'POST' and '/we/' in uri: action = 'Edição de Documento'
|
||||||
|
elif req_method == 'PUT': action = 'Upload (Web/Outros)'
|
||||||
|
elif req_method == 'GET' and '/remote.php/dav/files/' in uri: action = 'Download/Visualização (Web)'
|
||||||
|
elif req_method == 'DELETE': action = 'Exclusão de Arquivo/Pasta'
|
||||||
|
elif req_method == 'MKCOL': action = 'Criação de Pasta'
|
||||||
|
elif req_method == 'MOVE': action = 'Mover/Renomear Arquivo'
|
||||||
|
elif req_method == 'GET' and '/ocs/v2.php' in uri: action = 'Atividade em Segundo Plano (Web)'
|
||||||
|
elif req_method == 'POST': action = 'Interação Web (Formulários, etc.)'
|
||||||
|
elif req_method not in ['GET', 'HEAD']: action = f'Outra ({req_method})'
|
||||||
|
for category, actions in ACTION_CATEGORIES.items():
|
||||||
|
if action in actions: return action, category, client_type
|
||||||
|
return action, 'Outros', client_type
|
||||||
|
|
||||||
|
def parse_line(row: list) -> dict | None:
|
||||||
|
try:
|
||||||
|
if len(row) < 13: return None
|
||||||
|
timestamp = datetime.fromisoformat(row[0][:-6])
|
||||||
|
user_agent, request = row[2], row[9]
|
||||||
|
req_method = request.split(' ')[0]
|
||||||
|
uri = request.split(' ')[1] if len(request.split(' ')) > 1 else ''
|
||||||
|
action, category, client_type = get_action_details(req_method, uri, user_agent)
|
||||||
|
return { 'timestamp': timestamp, 'username': (re.search(r'nc_username=([^;]+)', row[3]).group(1).strip() if 'nc_username' in row[3] else None),
|
||||||
|
'category': category, 'client_type': client_type }
|
||||||
|
except (ValueError, TypeError, IndexError): return None
|
||||||
|
|
||||||
|
def generate_activity_scatter_chart(df: pd.DataFrame) -> str:
|
||||||
|
print("Gerando gráfico de dispersão...")
|
||||||
|
if df.empty: return ""
|
||||||
|
scatter_data = df.groupby([df['timestamp'].dt.floor('H'), 'category']).size().reset_index(name='counts')
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 6))
|
||||||
|
max_count = scatter_data['counts'].max()
|
||||||
|
size_factor = 2000 / max_count if max_count > 0 else 1
|
||||||
|
for category, color in CATEGORY_COLORS.items():
|
||||||
|
subset = scatter_data[scatter_data['category'] == category]
|
||||||
|
if not subset.empty:
|
||||||
|
ax.scatter(subset['timestamp'], subset['category'], s=subset['counts'] * size_factor, c=color, label=category, alpha=0.6, edgecolors="w", linewidth=0.5)
|
||||||
|
ax.set_title('Nuvens de Atividade por Categoria e Tempo', fontsize=16)
|
||||||
|
ax.xaxis.set_major_formatter(mdates.DateFormatter('%d/%m %Hh'))
|
||||||
|
ax.grid(axis='x', linestyle='--', alpha=0.5); ax.legend(title='Categorias', bbox_to_anchor=(1.05, 1), loc='upper left', markerscale=0.5)
|
||||||
|
plt.tight_layout(); img_buffer = io.BytesIO()
|
||||||
|
plt.savefig(img_buffer, format='png', dpi=100); plt.close(fig); img_buffer.seek(0)
|
||||||
|
return base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||||||
|
|
||||||
|
def generate_visualizations(df: pd.DataFrame) -> tuple[str, dict, list]:
|
||||||
|
print("Gerando visualizações de usuário...")
|
||||||
|
if df.empty: return "", {}, []
|
||||||
|
df['day_dt'] = pd.to_datetime(df['timestamp'].dt.date); df['hour'] = df['timestamp'].dt.hour
|
||||||
|
df['weekday'] = df['day_dt'].dt.day_name().map(DAY_MAP_PT); all_weekdays = list(DAY_MAP_PT.values())
|
||||||
|
overall_heatmap_b64, agg_heatmaps, daily_timelines = "", {}, []
|
||||||
|
|
||||||
|
heatmap_data = df.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(index=all_weekdays, columns=range(24), fill_value=0)
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 5)); sns.heatmap(heatmap_data, annot=False, cmap='viridis', ax=ax, cbar_kws={'label': 'Nº de Ações'})
|
||||||
|
ax.set_title('Padrão de Atividade Agregado (Todos os Colaboradores)', fontsize=12); plt.tight_layout(); img_buffer = io.BytesIO()
|
||||||
|
plt.savefig(img_buffer, format='png', dpi=90); plt.close(fig); img_buffer.seek(0)
|
||||||
|
overall_heatmap_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||||||
|
|
||||||
|
for user, user_df in df.groupby('username'):
|
||||||
|
user_agg_data = user_df.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(index=all_weekdays, columns=range(24), fill_value=0)
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 5)); sns.heatmap(user_agg_data, annot=False, cmap='viridis', ax=ax, cbar_kws={'label': 'Nº de Ações'})
|
||||||
|
ax.set_title(f'Padrão de Atividade Agregado - {user}', fontsize=12); plt.tight_layout(); img_buffer_agg = io.BytesIO()
|
||||||
|
plt.savefig(img_buffer_agg, format='png', dpi=90); plt.close(fig); img_buffer_agg.seek(0)
|
||||||
|
agg_heatmaps[user] = base64.b64encode(img_buffer_agg.getvalue()).decode('utf-8')
|
||||||
|
|
||||||
|
for (user, day), group in df.groupby(['username', df['day_dt'].dt.date]):
|
||||||
|
for client_type in ['Web', 'App']:
|
||||||
|
data = group[group['client_type'] == client_type].groupby(['hour', 'category']).size().unstack(fill_value=0).reindex(columns=ACTION_CATEGORIES.keys(), fill_value=0).reindex(index=range(24), fill_value=0)
|
||||||
|
if not data.empty and data.sum().sum() > 0:
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 4))
|
||||||
|
for category in data.columns:
|
||||||
|
x, y = data.index, data[category]
|
||||||
|
if y.sum() > 0:
|
||||||
|
x_smooth = np.linspace(x.min(), x.max(), 300); spl = make_interp_spline(x, y, k=3); y_smooth = spl(x_smooth)
|
||||||
|
ax.plot(x_smooth, y_smooth, color=CATEGORY_COLORS.get(category), label=category)
|
||||||
|
ax.plot(x, y, 'o', color=CATEGORY_COLORS.get(category), markersize=4)
|
||||||
|
ax.set_title(f'Timeline de Atividade {client_type.upper()} - {user} em {day.strftime("%d/%m/%Y")}', fontsize=12)
|
||||||
|
ax.set_xlabel('Hora do Dia'); ax.set_ylabel('Intensidade (Nº de Ações)'); ax.grid(axis='y', linestyle='--', alpha=0.7); ax.legend(title='Categorias', loc='upper left')
|
||||||
|
ax.set_xticks(range(0, 25, 2)); plt.tight_layout(); img_buffer_daily = io.BytesIO(); plt.savefig(img_buffer_daily, format='png', dpi=100); plt.close(fig); img_buffer_daily.seek(0)
|
||||||
|
daily_timelines.append({"user": user, "day": day.isoformat(), "client_type": client_type, "chart_b64": base64.b64encode(img_buffer_daily.getvalue()).decode('utf-8')})
|
||||||
|
return overall_heatmap_b64, agg_heatmaps, daily_timelines
|
||||||
|
|
||||||
|
def main(log_file_path: str):
|
||||||
|
print(f"Iniciando processamento do arquivo: {log_file_path}")
|
||||||
|
all_log_entries = []
|
||||||
|
total_lines = 0
|
||||||
|
with open(log_file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
reader = csv.reader(f, delimiter=',', quotechar='"')
|
||||||
|
try: next(reader); total_lines = 1
|
||||||
|
except StopIteration: pass
|
||||||
|
for row in reader:
|
||||||
|
total_lines += 1; parsed = parse_line(row)
|
||||||
|
if parsed and parsed['username']: all_log_entries.append(parsed)
|
||||||
|
df = pd.DataFrame(all_log_entries)
|
||||||
|
parsed_lines = len(df); print(f"\n--- Processamento Concluído ---\nLinhas Lidas: {total_lines:,} | Requisições Válidas com Usuário: {parsed_lines:,}")
|
||||||
|
scatter_chart_b64, overall_heatmap_b64, agg_heatmaps, daily_timelines = "", "", {}, []
|
||||||
|
if not df.empty:
|
||||||
|
scatter_chart_b64 = generate_activity_scatter_chart(df)
|
||||||
|
overall_heatmap_b64, agg_heatmaps, daily_timelines = generate_visualizations(df)
|
||||||
|
generate_html_report(scatter_chart_b64, overall_heatmap_b64, agg_heatmaps, daily_timelines, total_lines, parsed_lines)
|
||||||
|
|
||||||
|
def generate_html_report(scatter_chart_b64, overall_heatmap_b64, agg_heatmaps, daily_timelines, total_lines, parsed_lines):
|
||||||
|
print("\nGerando relatório HTML final...")
|
||||||
|
activity_map = {}; user_daily_html_parts = []
|
||||||
|
for item in daily_timelines: activity_map.setdefault(item['user'], []).append(item['day'])
|
||||||
|
user_agg_html = "".join([f'<div class="user-agg-heatmap-card" data-user="{user}" style="display:none;"><img src="data:image/png;base64,{b64}" alt="Heatmap para {user}"></div>' for user, b64 in agg_heatmaps.items()])
|
||||||
|
for item in daily_timelines:
|
||||||
|
user_daily_html_parts.append(f'<div class="daily-chart-card" data-user="{item["user"]}" data-day="{item["day"]}" style="display:none;"><img src="data:image/png;base64,{item["chart_b64"]}" alt="Timeline para {item["user"]}"></div>')
|
||||||
|
user_daily_html = "".join(user_daily_html_parts)
|
||||||
|
generation_time = datetime.now().strftime('%d de %B de %Y, %H:%M:%S')
|
||||||
|
|
||||||
|
html_template = f"""
|
||||||
|
<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Painel de Atividade</title><style>body{{font-family:Arial,sans-serif;background-color:#f4f6f9;margin:0;padding:0;color:#333}} .container{{max-width:1200px;margin:auto;padding:15px}} h1,h2{{color:#2c3e50;border-bottom:2px solid #3498db;padding-bottom:10px}} .header-info{{text-align:right;font-size:0.9em;color:#777;margin-top:-30px;margin-bottom:20px}} .card{{background-color:white;border:1px solid #e0e0e0;box-shadow:0 2px 4px rgba(0,0,0,0.05);padding:20px;margin-bottom:20px;border-radius:5px}} .filter-controls{{display:flex;flex-wrap:wrap;gap:15px;align-items:center;margin-bottom:20px}} .filter-controls label{{font-weight:bold}} .filter-controls select{{padding:8px;border-radius:4px;border:1px solid #ccc;min-width:180px}} .chart-container img{{max-width:100%;height:auto}} footer{{text-align:center;margin-top:20px;padding:15px;font-size:0.9em;color:#777}} #detailed-legend{{display:none}} @media (max-width: 768px){{ .header-info{{text-align:left;margin-top:10px;margin-bottom:10px}} .filter-controls{{flex-direction:column;align-items:stretch}} }}</style></head><body><div class="container">
|
||||||
|
<header><h1>Painel de Atividade nos Sistemas</h1><p class="header-info"><strong>Gerado em:</strong> {generation_time}</p></header>
|
||||||
|
<section class="card"><h2>Resumo de Atividade</h2><div class="chart-container"><img src="data:image/png;base64,{scatter_chart_b64}" alt="Gráfico de Dispersão"></div></section>
|
||||||
|
<section class="card"><h2>Análise de Atividade Individual</h2><div class="filter-controls"><label for="user-filter">Colaborador:</label><select id="user-filter"><option value="all">Visão Agregada (Todos)</option>{"".join(f'<option value="{u}">{u}</option>' for u in sorted(activity_map.keys()))}</select><label for="day-filter">Dia de Atividade:</label><select id="day-filter" disabled><option value="all">Padrão Agregado</option></select></div><div id="user-viz-container"><div id="overall-agg-heatmap" style="display:block;"><img src="data:image/png;base64,{overall_heatmap_b64}" alt="Heatmap Geral"></div>{user_agg_html}{user_daily_html}<div id="detailed-legend">{DETAILED_LEGEND_HTML}</div></div></section>
|
||||||
|
<footer><p>Relatório gerado automaticamente.</p></footer></div>
|
||||||
|
<script>
|
||||||
|
const activityMap = {json.dumps({u: sorted(list(set(d))) for u, d in activity_map.items()})};
|
||||||
|
const userFilter = document.getElementById('user-filter'); const dayFilter = document.getElementById('day-filter');
|
||||||
|
const legend = document.getElementById('detailed-legend');
|
||||||
|
function updateDayFilter(){{ const selectedUser=userFilter.value; dayFilter.innerHTML='<option value="all">Padrão Agregado</option>'; if(selectedUser!=='all'&&activityMap[selectedUser]){{ activityMap[selectedUser].forEach(day=>{{ const date=new Date(day+'T00:00:00Z'); const formattedDate=date.toLocaleDateString('pt-BR',{{timeZone:'UTC'}}); dayFilter.innerHTML+=`<option value="${{day}}">${{formattedDate}}</option>`; }}); dayFilter.disabled=false; }} else{{ dayFilter.disabled=true; }} }}
|
||||||
|
function filterContent(){{ const selectedUser=userFilter.value; const selectedDay=dayFilter.value; document.getElementById('overall-agg-heatmap').style.display='none'; document.querySelectorAll('.user-agg-heatmap-card').forEach(c=>c.style.display='none'); document.querySelectorAll('.daily-chart-card').forEach(c=>c.style.display='none'); legend.style.display='none'; if(selectedUser==='all'){{ document.getElementById('overall-agg-heatmap').style.display='block'; }} else if(selectedDay==='all'){{ const card=document.querySelector(`.user-agg-heatmap-card[data-user="${{selectedUser}}"]`); if(card)card.style.display='block'; }} else{{ document.querySelectorAll(`.daily-chart-card[data-user="${{selectedUser}}"][data-day="${{selectedDay}}"]`).forEach(c=>c.style.display='block'); legend.style.display='block'; }} }}
|
||||||
|
userFilter.addEventListener('change',()=>{{updateDayFilter();filterContent();}}); dayFilter.addEventListener('change',filterContent);
|
||||||
|
</script></body></html>
|
||||||
|
"""
|
||||||
|
with open('painel_de_atividade_final.html', 'w', encoding='utf-8') as f: f.write(html_template)
|
||||||
|
print("\nRelatório final 'Painel de Atividade' salvo com sucesso!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
LOG_FILE = r'C:\Users\joao.goncalves\Documents\Relatorio FDP\nginx_logs_final.csv'
|
||||||
|
main(LOG_FILE)
|
||||||
Loading…
Reference in New Issue