feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
|
|
@ -0,0 +1,35 @@
|
|||
# Specification Quality Checklist: Analytics Dashboard (Admin)
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-14
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Spec gerado com base em descrição detalhada fornecida pelo usuário
|
||||
- Todos os itens passaram na validação inicial — feature pronta para `/speckit.plan`
|
||||
341
.specify/features/016-analytics-dashboard/plan.md
Normal file
341
.specify/features/016-analytics-dashboard/plan.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
# Implementation Plan: Analytics Dashboard (Admin)
|
||||
|
||||
**Branch**: `016-analytics-dashboard` | **Date**: 2026-04-14 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from `.specify/features/016-analytics-dashboard/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Adicionar um dashboard de analytics ao painel admin que exibe métricas de acesso ao site (total por dia/semana/mês, gráfico de linha dos últimos N dias, top 10 páginas e top 10 imóveis mais visitados). O rastreamento é feito via `before_request` hook no Flask — sem bibliotecas de charting externas no frontend (SVG/CSS com Tailwind tokens existentes).
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.12 (backend) · TypeScript 5.5 (frontend)
|
||||
**Primary Dependencies**: Flask 3.x, SQLAlchemy 2.x, Pydantic v2, PyJWT (backend) · React 18, Tailwind CSS 3.4, Axios (frontend)
|
||||
**Storage**: PostgreSQL 16 — nova tabela `page_views`
|
||||
**Testing**: pytest (backend) · Vite build check (frontend)
|
||||
**Target Platform**: Web service (REST API) + SPA
|
||||
**Project Type**: Web application (backend + frontend)
|
||||
**Performance Goals**: Resposta do dashboard < 3 s (SC-001); overhead do hook < 50 ms por requisição (SC-002)
|
||||
**Constraints**: IP jamais persistido em claro — somente SHA-256 hash (FR-010, SC-005); rastreamento falha silenciosamente sem bloquear visitante (FR-009); somente admins autenticados acessam endpoints (FR-011)
|
||||
**Scale/Scope**: Volume proporcional ao tráfego; top-10 fixo; sem paginação em v1; sem polling automático em v1
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Princípio | Status | Observação |
|
||||
|-----------|--------|------------|
|
||||
| I. Design-First | PASS | Frontend usa tokens Tailwind do sistema (bg-canvas, bg-panel, bg-surface, text-textPrimary, border-borderPrimary, bg-brand etc.); gráfico de linha via SVG inline — sem libs externas |
|
||||
| II. Separação de Responsabilidades | PASS | Flask expõe 3 endpoints REST JSON puros; React consome via Axios; sem SSR |
|
||||
| III. Spec-Driven | PASS | spec.md com user stories e acceptance scenarios aprovado; spec → plan → tasks → implement |
|
||||
| IV. Integridade de Dados | PASS | Migração Alembic para page_views; SQLAlchemy ORM para queries; Pydantic v2 nos schemas; property_id nullable sem FK (preserva histórico se imóvel deletado) |
|
||||
| V. Segurança | PASS | Endpoints /api/v1/admin/analytics/* protegidos por require_admin; IP armazenado como SHA-256 hash irreversível com salt; rotas admin e auth excluídas do rastreamento (FR-003) |
|
||||
| VI. Simplicidade | PASS | before_request no app factory sem camada extra; SVG puro sem Recharts/Chart.js; 3 endpoints de agregação SQL simples |
|
||||
|
||||
**Constitution Check Post-Design**: PASS — nenhuma violação identificada após Phase 1.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```
|
||||
.specify/features/016-analytics-dashboard/
|
||||
├── plan.md # Este arquivo
|
||||
├── research.md # Phase 0 output
|
||||
├── data-model.md # Phase 1 output
|
||||
├── contracts/
|
||||
│ └── analytics-api.md # Phase 1 output
|
||||
├── quickstart.md # Phase 1 output
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks — NÃO criado por /speckit.plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── models/
|
||||
│ │ └── page_view.py # NOVO — modelo PageView (tabela page_views)
|
||||
│ ├── routes/
|
||||
│ │ └── analytics.py # NOVO — Blueprint analytics_bp, 3 endpoints GET
|
||||
│ ├── schemas/
|
||||
│ │ └── analytics.py # NOVO — Pydantic schemas de resposta
|
||||
│ └── __init__.py # MODIFICADO — importa model, registra blueprint, adds before_request hook
|
||||
├── migrations/versions/
|
||||
│ └── xxxx_add_page_views.py # NOVO — Alembic migration up/down
|
||||
└── tests/
|
||||
└── test_analytics.py # NOVO — 9 cenários de teste
|
||||
|
||||
frontend/
|
||||
└── src/
|
||||
├── pages/admin/
|
||||
│ └── AdminAnalyticsPage.tsx # NOVO — cards, gráfico SVG, tabelas
|
||||
├── services/
|
||||
│ └── analytics.ts # NOVO — 3 funções Axios tipadas
|
||||
└── components/
|
||||
└── Navbar.tsx # MODIFICADO — link "Analytics" na nav admin
|
||||
```
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> Nenhuma violação da Constituição identificada.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Research
|
||||
|
||||
*Saída em: [research.md](research.md)*
|
||||
|
||||
### Unknowns Resolvidos
|
||||
|
||||
#### 1. Estratégia de rastreamento: before_request vs. after_request
|
||||
|
||||
- **Decisão**: `before_request` com commit dentro de `try/except` que engole todas as exceções.
|
||||
- **Racional**: Não bloqueia a resposta ao visitante (FR-009). `before_request` é mais simples — rota e método já estão disponíveis sem inspecionar o response object.
|
||||
- **Alternativas descartadas**: Celery/queue assíncrona — complexidade desnecessária para o volume esperado (YAGNI, Princípio VI).
|
||||
|
||||
#### 2. Anonimização do IP
|
||||
|
||||
- **Decisão**: `hashlib.sha256((ip + salt).encode()).hexdigest()` onde salt = `app.config["IP_SALT"]` (variável de ambiente `IP_SALT`, obrigatória, sem padrão).
|
||||
- **Racional**: Hash irreversível + salt impede rainbow-table; atende SC-005 e FR-010.
|
||||
- **Alternativas descartadas**: Truncar IP (reversível), MD5 (colisões conhecidas), bcrypt (lento demais para before_request).
|
||||
|
||||
#### 3. Extração do property_id do caminho
|
||||
|
||||
- **Decisão**: Regex `r"^/api/v1/properties/([0-9a-f-]{36})$"` aplicado sobre `request.path` dentro do hook.
|
||||
- **Racional**: A rota de detalhe (`/api/v1/properties/<uuid>`) é o único endpoint a associar `property_id`. Regex simples sem importar o mapa interno de rotas do Flask.
|
||||
- **Alternativas descartadas**: `request.view_args` (nem sempre disponível em before_request cross-blueprint).
|
||||
|
||||
#### 4. Gráfico de linha sem biblioteca externa
|
||||
|
||||
- **Decisão**: SVG inline gerado no componente React com `<polyline>` calculado a partir de `daily_series`. Eixo X = dias, eixo Y normalizado pela altura do viewBox.
|
||||
- **Racional**: Sem nova dependência npm (Princípio VI). Recharts/Chart.js adicionariam >200 KB ao bundle sem justificativa para o escopo da feature.
|
||||
- **Alternativas descartadas**: Recharts (dependência nova injustificada), bar chart CSS-only (menos informativo para série temporal contínua).
|
||||
|
||||
#### 5. Filtro de período no frontend
|
||||
|
||||
- **Decisão**: Estado local `days: 7 | 30 | 90` na página; passado como query param `?days=N` para os 3 endpoints.
|
||||
- **Racional**: Spec FR-008 define exatamente 3 períodos fixos; query param simples é suficiente.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Design & Contracts
|
||||
|
||||
*Saídas em: [data-model.md](data-model.md) · [contracts/analytics-api.md](contracts/analytics-api.md) · [quickstart.md](quickstart.md)*
|
||||
|
||||
### Data Model — tabela `page_views`
|
||||
|
||||
| Coluna | Tipo | Nullable | Notas |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | UUID (PK, default gen_random_uuid()) | NO | |
|
||||
| `path` | VARCHAR(500) | NO | Caminho da URL, ex: `/imoveis/slug` |
|
||||
| `property_id` | UUID (sem FK constraint) | YES | Preenchido apenas para rotas de detalhe de imóvel |
|
||||
| `accessed_at` | TIMESTAMP (server default now()) | NO | **Indexado** — base de todas as queries por período |
|
||||
| `ip_hash` | VARCHAR(64) | NO | SHA-256 hex do (IP + salt) |
|
||||
| `user_agent` | VARCHAR(500) | YES | Truncado em 500 chars |
|
||||
|
||||
**Índices**: `ix_page_views_accessed_at`, `ix_page_views_property_id`.
|
||||
|
||||
**Sem FK em property_id**: preserva registros históricos mesmo após remoção de imóveis.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
**Blueprint**: `analytics_bp`, montado em `/api/v1/admin` → prefixo efetivo `/api/v1/admin/analytics`
|
||||
|
||||
Todos os endpoints exigem `Authorization: Bearer <token>` de administrador (`require_admin`).
|
||||
|
||||
#### GET /api/v1/admin/analytics/summary?days=30
|
||||
|
||||
Cards de métricas + série temporal diária.
|
||||
|
||||
```json
|
||||
{
|
||||
"today": 42,
|
||||
"this_week": 310,
|
||||
"this_month": 1204,
|
||||
"period_total": 1204,
|
||||
"daily_series": [
|
||||
{ "date": "2026-03-16", "count": 38 },
|
||||
{ "date": "2026-03-17", "count": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `today`, `this_week`, `this_month`: calculados a partir da data corrente, independente de `days`.
|
||||
- `daily_series`: exatamente `days` entradas; dias sem dados têm `count: 0`.
|
||||
|
||||
#### GET /api/v1/admin/analytics/top-pages?days=30
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{ "path": "/imoveis/apartamento-centro", "count": 87 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Top 10 caminhos por contagem de acessos no período.
|
||||
|
||||
#### GET /api/v1/admin/analytics/top-properties?days=30
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"property_id": "uuid",
|
||||
"title": "Apartamento Centro",
|
||||
"cover_photo": "/uploads/foto.jpg",
|
||||
"count": 87
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- JOIN com `properties` para obter `title` e `cover_photo` (primeira foto da relação `photos`).
|
||||
- LEFT JOIN — se imóvel não existir mais, o item é omitido do resultado.
|
||||
|
||||
### Before-Request Hook (rastreamento)
|
||||
|
||||
Inserido em `create_app()` após o registro do `analytics_bp`:
|
||||
|
||||
```python
|
||||
import hashlib, re
|
||||
from app.models.page_view import PageView
|
||||
|
||||
@app.before_request
|
||||
def _track_page_view():
|
||||
try:
|
||||
if request.method != "GET":
|
||||
return
|
||||
path = request.path
|
||||
excluded = ("/api/v1/admin", "/api/v1/auth", "/static")
|
||||
if any(path.startswith(p) for p in excluded):
|
||||
return
|
||||
ip_raw = request.remote_addr or ""
|
||||
salt = current_app.config.get("IP_SALT", "")
|
||||
ip_hash = hashlib.sha256((ip_raw + salt).encode()).hexdigest()
|
||||
property_id = None
|
||||
m = re.match(r"^/api/v1/properties/([0-9a-f-]{36})$", path)
|
||||
if m:
|
||||
property_id = m.group(1)
|
||||
pv = PageView(
|
||||
path=path[:500],
|
||||
ip_hash=ip_hash,
|
||||
user_agent=(request.user_agent.string or "")[:500],
|
||||
property_id=property_id,
|
||||
)
|
||||
db.session.add(pv)
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
```
|
||||
|
||||
### Pydantic Schemas (`backend/app/schemas/analytics.py`)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DailyPoint(BaseModel):
|
||||
date: str # "YYYY-MM-DD"
|
||||
count: int
|
||||
|
||||
class AnalyticsSummary(BaseModel):
|
||||
today: int
|
||||
this_week: int
|
||||
this_month: int
|
||||
period_total: int
|
||||
daily_series: list[DailyPoint]
|
||||
|
||||
class TopPageItem(BaseModel):
|
||||
path: str
|
||||
count: int
|
||||
|
||||
class TopPagesResponse(BaseModel):
|
||||
items: list[TopPageItem]
|
||||
|
||||
class TopPropertyItem(BaseModel):
|
||||
property_id: str
|
||||
title: str
|
||||
cover_photo: str | None
|
||||
count: int
|
||||
|
||||
class TopPropertiesResponse(BaseModel):
|
||||
items: list[TopPropertyItem]
|
||||
```
|
||||
|
||||
### Alembic Migration (esqueleto)
|
||||
|
||||
```python
|
||||
# upgrade
|
||||
op.create_table(
|
||||
"page_views",
|
||||
sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("path", sa.String(500), nullable=False),
|
||||
sa.Column("property_id", sa.UUID(), nullable=True),
|
||||
sa.Column("accessed_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("ip_hash", sa.String(64), nullable=False),
|
||||
sa.Column("user_agent", sa.String(500), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_page_views_accessed_at", "page_views", ["accessed_at"])
|
||||
op.create_index("ix_page_views_property_id", "page_views", ["property_id"])
|
||||
|
||||
# downgrade
|
||||
op.drop_index("ix_page_views_property_id", table_name="page_views")
|
||||
op.drop_index("ix_page_views_accessed_at", table_name="page_views")
|
||||
op.drop_table("page_views")
|
||||
```
|
||||
|
||||
### Frontend — AdminAnalyticsPage.tsx
|
||||
|
||||
**Rota**: `/admin/analytics` (protegida por `AdminRoute`, adicionada no router em `App.tsx`)
|
||||
|
||||
**Componentes internos** (inline na página — sem arquivos separados por simplicidade v1):
|
||||
- `PeriodFilter`: botões "7d / 30d / 90d", ativo com `bg-brand text-white`; inativo com `bg-surface text-textSecondary`
|
||||
- `MetricCard`: `bg-panel border border-borderSubtle shadow-card rounded-lg`; valor em `text-textPrimary text-2xl font-semibold`; label em `text-textSecondary text-sm`
|
||||
- `LineChart`: SVG `viewBox="0 0 600 120"`, `<polyline>` calculado; linha em `stroke="#7170ff"`; área de fundo com `<polygon>` opacity 0.1 `fill="#5e6ad2"`; eixo X com labels de data
|
||||
- `TopPagesTable` e `TopPropertiesTable`: `bg-panel`, linhas alternadas com `bg-surface`, `text-textPrimary` e `text-textSecondary`
|
||||
|
||||
**Serviço (`frontend/src/services/analytics.ts`)**:
|
||||
```typescript
|
||||
import { api } from './api'
|
||||
|
||||
export const getAnalyticsSummary = (days: number) =>
|
||||
api.get<AnalyticsSummary>(`/admin/analytics/summary?days=${days}`)
|
||||
|
||||
export const getTopPages = (days: number) =>
|
||||
api.get<TopPagesResponse>(`/admin/analytics/top-pages?days=${days}`)
|
||||
|
||||
export const getTopProperties = (days: number) =>
|
||||
api.get<TopPropertiesResponse>(`/admin/analytics/top-properties?days=${days}`)
|
||||
```
|
||||
|
||||
### Testes pytest — 9 cenários
|
||||
|
||||
| # | Cenário |
|
||||
|---|---------|
|
||||
| 1 | `GET /summary` sem token → 401 |
|
||||
| 2 | `GET /summary` com token admin → 200, shape correto |
|
||||
| 3 | `GET /summary` com 0 registros → campos zerados, `daily_series` com `count: 0` para cada dia |
|
||||
| 4 | `GET /top-pages` retorna apenas acessos dentro do período (`days`) |
|
||||
| 5 | `GET /top-properties` LEFT JOIN correto — imóvel inexistente omitido |
|
||||
| 6 | Hook NOT registra acesso a `/api/v1/admin/*` |
|
||||
| 7 | Hook NOT registra acesso a `/api/v1/auth/*` |
|
||||
| 8 | Hook registra acesso a rota pública e cria `PageView` no banco |
|
||||
| 9 | Falha de banco no hook não propaga exceção para o cliente |
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Entrega
|
||||
|
||||
- [ ] `backend/app/models/page_view.py` criado e importado em `__init__.py`
|
||||
- [ ] Hook `before_request` em `create_app()`, excluindo `/api/v1/admin/*`, `/api/v1/auth/*`, `/static`
|
||||
- [ ] Migração Alembic criada e testada (upgrade + downgrade)
|
||||
- [ ] 3 endpoints GET em `backend/app/routes/analytics.py`, protegidos por `require_admin`
|
||||
- [ ] Pydantic schemas em `backend/app/schemas/analytics.py`
|
||||
- [ ] Testes em `backend/tests/test_analytics.py` (9 cenários)
|
||||
- [ ] `frontend/src/services/analytics.ts` com 3 funções tipadas
|
||||
- [ ] `AdminAnalyticsPage.tsx` com cards, gráfico SVG e tabelas usando tokens do design system
|
||||
- [ ] Link "Analytics" adicionado no `Navbar.tsx` (área admin)
|
||||
- [ ] Rota `/admin/analytics` registrada no router em `App.tsx`
|
||||
- [ ] Variável de ambiente `IP_SALT` documentada como obrigatória
|
||||
118
.specify/features/016-analytics-dashboard/spec.md
Normal file
118
.specify/features/016-analytics-dashboard/spec.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Feature Specification: Analytics Dashboard (Admin)
|
||||
|
||||
**Feature Branch**: `016-analytics-dashboard`
|
||||
**Created**: 2026-04-14
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Dashboard de Analytics no painel admin para acompanhar acessos ao site."
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Visualizar Métricas de Acesso (Priority: P1)
|
||||
|
||||
O administrador do sistema acessa a página de analytics no painel admin e visualiza um resumo dos acessos ao site: total de visitas hoje, nesta semana e neste mês, além de um gráfico mostrando a evolução de acessos nos últimos 30 dias.
|
||||
|
||||
**Why this priority**: É o coração do dashboard — sem essa visão consolidada, o administrador não consegue avaliar o desempenho do site. Entrega valor imediato ao permitir compreender tendências de tráfego.
|
||||
|
||||
**Independent Test**: Pode ser testado de forma completa acessando `/admin/analytics` após algumas visitas registradas. O administrador deve ver os cards de métricas e o gráfico com dados reais.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** o administrador está autenticado e existem registros de acesso, **When** ele acessa `/admin/analytics`, **Then** vê cards com total de acessos de hoje, da semana atual e do mês atual.
|
||||
2. **Given** o administrador está na página de analytics, **When** a página carrega, **Then** um gráfico de linha exibe os acessos diários dos últimos 30 dias.
|
||||
3. **Given** o administrador está na página de analytics, **When** ele altera o filtro de período para "7 dias", "30 dias" ou "90 dias", **Then** todas as métricas e gráficos atualizam para refletir o período selecionado.
|
||||
4. **Given** não existe nenhum acesso registrado, **When** o administrador acessa o dashboard, **Then** vê métricas zeradas e mensagem indicando ausência de dados.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Consultar Páginas e Imóveis Mais Acessados (Priority: P2)
|
||||
|
||||
O administrador consulta as tabelas que listam as 10 páginas e os 10 imóveis mais visitados do site, para entender quais conteúdos são mais relevantes para os visitantes.
|
||||
|
||||
**Why this priority**: Permite decisões de negócio — quais imóveis destacar, quais páginas otimizar. Depende dos dados de rastreamento já coletados (P1).
|
||||
|
||||
**Independent Test**: Testável de forma independente realizando múltiplas visitas a páginas e imóveis distintos e verificando o ranking exibido no dashboard.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** existem registros de acesso a múltiplas páginas, **When** o administrador visualiza o dashboard, **Then** uma tabela exibe as 10 páginas mais acessadas com o caminho e o total de visitas, ordenadas do maior para o menor.
|
||||
2. **Given** existem registros de visualização de imóveis, **When** o administrador visualiza o dashboard, **Then** uma tabela exibe os 10 imóveis mais vistos com título, foto de capa e total de visualizações.
|
||||
3. **Given** há menos de 10 páginas/imóveis distintos com registros, **When** o administrador visualiza as tabelas, **Then** a tabela exibe apenas os itens disponíveis (sem erros ou linhas vazias).
|
||||
4. **Given** o administrador aplica um filtro de período, **When** filtra por "7 dias", **Then** as tabelas de top páginas e top imóveis refletem somente os acessos dentro do período.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Rastreamento Automático de Visitas Públicas (Priority: P3)
|
||||
|
||||
Cada vez que um visitante acessa uma página pública do site, o sistema registra automaticamente essa visita sem exigir nenhuma ação do usuário, preservando a privacidade ao armazenar um hash do IP em vez do IP completo.
|
||||
|
||||
**Why this priority**: É o mecanismo que alimenta todas as métricas do dashboard. Sem rastreamento, não há dados a exibir. Classificado como P3 pois pode ser implementado em paralelo com as histórias de UI, e os dados iniciais podem ser simulados nos testes.
|
||||
|
||||
**Independent Test**: Testável acessando páginas públicas e verificando no banco de dados que os registros foram criados com os campos corretos (path, timestamp, hash de IP, user agent).
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um visitante acessa qualquer página pública (home, listagem, detalhe, sobre, contato), **When** a página é carregada, **Then** um registro de acesso é criado com caminho, horário, hash do IP e user agent.
|
||||
2. **Given** um visitante acessa a página de detalhe de um imóvel, **When** a página carrega, **Then** o registro de acesso é associado ao ID do imóvel correspondente, incrementando seu contador de visualizações.
|
||||
3. **Given** uma requisição é feita para rotas administrativas (`/admin/*`) ou de autenticação (`/api/auth/*`), **When** qualquer usuário acessa essas rotas, **Then** nenhum registro de acesso é criado (rastreamento excluído).
|
||||
4. **Given** o mesmo visitante acessa múltiplas páginas em sequência, **When** os acessos são registrados, **Then** cada acesso gera um registro separado com seu próprio timestamp.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- O que acontece se o banco de dados estiver indisponível no momento do registro de uma visita? O acesso do visitante não deve ser bloqueado — o rastreamento deve falhar silenciosamente.
|
||||
- Como o sistema lida com bots/crawlers? User agent é armazenado mas não há filtro automático de bots na v1 (pode ser adicionado posteriormente).
|
||||
- O que acontece se um imóvel referenciado em um acesso for deletado? O registro de acesso deve ser mantido, com `property_id` referenciando um imóvel inexistente (integridade referencial relaxada ou via soft delete).
|
||||
- Como se comporta o gráfico se houver dias sem nenhum acesso no período selecionado? O gráfico deve exibir esses dias com valor zero, sem lacunas na linha do tempo.
|
||||
- O dashboard exibe dados em tempo real ou com delay? Os dados refletem os acessos até o momento da consulta (sem polling automático na v1).
|
||||
|
||||
---
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: O sistema DEVE registrar automaticamente cada acesso a páginas públicas com: caminho da URL, data/hora do acesso, user agent e hash anonimizado do IP.
|
||||
- **FR-002**: O sistema DEVE associar o registro de acesso ao ID do imóvel quando a página visitada for a de detalhe de um imóvel.
|
||||
- **FR-003**: O sistema NÃO DEVE registrar acessos a rotas administrativas (`/admin/*`) nem a rotas de autenticação (`/api/auth/*`).
|
||||
- **FR-004**: O sistema DEVE disponibilizar ao administrador cards de métricas com total de acessos do dia atual, da semana atual e do mês atual.
|
||||
- **FR-005**: O sistema DEVE disponibilizar ao administrador um gráfico de linha com o total de acessos por dia para o período selecionado.
|
||||
- **FR-006**: O sistema DEVE disponibilizar ao administrador uma tabela com as 10 páginas mais acessadas (caminho + total de acessos) para o período selecionado.
|
||||
- **FR-007**: O sistema DEVE disponibilizar ao administrador uma tabela com os 10 imóveis mais visualizados (título + foto de capa + total de visualizações) para o período selecionado.
|
||||
- **FR-008**: O administrador DEVE conseguir filtrar todas as métricas e rankings por período: últimos 7 dias, últimos 30 dias ou últimos 90 dias.
|
||||
- **FR-009**: O rastreamento de acessos NÃO DEVE impactar a disponibilidade das páginas públicas — falhas no registro devem ser silenciosas para o visitante.
|
||||
- **FR-010**: O IP do visitante DEVE ser armazenado exclusivamente como hash irreversível, nunca em texto claro.
|
||||
- **FR-011**: Somente administradores autenticados DEVEM ter acesso aos dados de analytics.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Registro de Acesso (PageView)**: Representa uma visita a uma página pública. Atributos: identificador único, caminho da URL visitada, referência opcional ao imóvel visualizado, data/hora do acesso, hash do IP do visitante, user agent do navegador.
|
||||
- **Resumo de Analytics (Summary)**: Agregação calculada a partir dos registros de acesso, contendo totais por período (dia/semana/mês) e séries temporais diárias.
|
||||
- **Ranking de Páginas**: Lista ordenada das páginas com maior número de acessos em um período, com caminho e contagem.
|
||||
- **Ranking de Imóveis**: Lista ordenada dos imóveis com maior número de visualizações em um período, com título, foto e contagem.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: O administrador consegue visualizar as métricas de acesso em menos de 3 segundos após acessar a página de analytics.
|
||||
- **SC-002**: 100% dos acessos a páginas públicas são registrados sem impactar o tempo de resposta percebido pelo visitante (registro assíncrono ou com overhead inferior a 50ms por requisição).
|
||||
- **SC-003**: O dashboard exibe corretamente métricas zeradas quando não há dados para o período selecionado, sem erros ou telas em branco.
|
||||
- **SC-004**: A troca de filtro de período atualiza todas as métricas, gráfico e tabelas em menos de 2 segundos.
|
||||
- **SC-005**: Nenhum dado pessoal identificável (IP completo) é armazenado — auditoria deve confirmar que apenas hashes são persistidos.
|
||||
- **SC-006**: Acessos a rotas de administração (`/admin/*`) não aparecem nos registros de analytics após 1 semana de uso em produção.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- O sistema de autenticação de administradores já está implementado e funcional (feature 005/007).
|
||||
- A tabela de imóveis (`properties`) já existe com os campos de título e foto de capa.
|
||||
- O painel admin já possui layout e navegação estabelecidos — a página de analytics será adicionada como nova rota dentro da estrutura existente.
|
||||
- A privacidade de IPs via hash é suficiente para conformidade com boas práticas; conformidade formal com LGPD/GDPR fica fora do escopo desta feature.
|
||||
- Não há requisito de exportação de dados (CSV/PDF) nesta versão.
|
||||
- Filtros de período são globais — não há filtro por tipo de dispositivo, origem de tráfego ou localização geográfica nesta versão (v1).
|
||||
- Os dados de analytics não precisam de paginação na v1 — top 10 é o limite exibido nas tabelas.
|
||||
- O rastreamento é implementado no lado do servidor (middleware backend), não no frontend, para garantir consistência mesmo em casos de bloqueadores de JavaScript.
|
||||
199
.specify/features/016-analytics-dashboard/tasks.md
Normal file
199
.specify/features/016-analytics-dashboard/tasks.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Tasks: Analytics Dashboard (Admin)
|
||||
|
||||
**Input**: Design documents from `.specify/features/016-analytics-dashboard/`
|
||||
**Branch**: `016-analytics-dashboard`
|
||||
**Prerequisites**: plan.md ✓, spec.md ✓
|
||||
|
||||
**Organization**: Tasks organizadas por user story para permitir implementação e teste independentes de cada história.
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
- **[P]**: Pode rodar em paralelo (arquivos diferentes, sem dependências incompletas)
|
||||
- **[Story]**: User story correspondente ([US1], [US2], [US3])
|
||||
- Todos os caminhos são relativos à raiz do repositório
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup
|
||||
|
||||
**Purpose**: Configuração obrigatória que deve existir antes de qualquer implementação.
|
||||
|
||||
- [ ] T001 Add `IP_SALT` env variable to `backend/app/config.py` — em `BaseConfig` adicionar `IP_SALT = os.environ.get("IP_SALT", "")` e em `TestingConfig` adicionar `IP_SALT = "test-salt-analytics-016"` para evitar falha silenciosa durante testes
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Infraestrutura compartilhada por todas as user stories — modelo, migração, schemas e blueprint.
|
||||
|
||||
**⚠️ CRITICAL**: Nenhuma user story pode ser implementada até esta fase estar completa.
|
||||
|
||||
- [ ] T002 Create `backend/app/models/page_view.py` — modelo SQLAlchemy `PageView` com `__tablename__ = "page_views"` e colunas: `id` (`db.UUID(as_uuid=True)`, PK, `default=uuid.uuid4`), `path` (`db.String(500)`, not null), `property_id` (`db.UUID(as_uuid=True)`, nullable, **sem ForeignKey**), `accessed_at` (`db.DateTime`, not null, `default=datetime.utcnow`), `ip_hash` (`db.String(64)`, not null), `user_agent` (`db.String(500)`, nullable); declarar índices via `__table_args__` com `db.Index("ix_page_views_accessed_at", "accessed_at")` e `db.Index("ix_page_views_property_id", "property_id")`
|
||||
|
||||
- [ ] T003 [P] Create `backend/app/schemas/analytics.py` — schemas Pydantic v2 (`from pydantic import BaseModel`): `DailyPoint(date: str, count: int)`, `AnalyticsSummary(today: int, this_week: int, this_month: int, period_total: int, daily_series: list[DailyPoint])`, `TopPageItem(path: str, count: int)`, `TopPagesResponse(items: list[TopPageItem])`, `TopPropertyItem(property_id: str, title: str, cover_photo: str | None, count: int)`, `TopPropertiesResponse(items: list[TopPropertyItem])`
|
||||
|
||||
- [ ] T004 [P] Create `backend/app/routes/analytics.py` — scaffold do blueprint: `analytics_bp = Blueprint("analytics", __name__)`; importar `require_admin` de `app.utils.auth`, `db` de `app.extensions`, `PageView` de `app.models.page_view`, todos os schemas de `app.schemas.analytics`; criar as 3 funções de endpoint decoradas com `@analytics_bp.get(...)` e `@require_admin` ainda retornando `({}, 200)` como placeholder (corpo implementado em T007, T012, T013)
|
||||
|
||||
- [ ] T005 Update `backend/app/__init__.py` — na seção de import de models adicionar `from app.models import page_view as _page_view_models # noqa: F401`; na seção de registro de blueprints adicionar `from app.routes.analytics import analytics_bp` e `app.register_blueprint(analytics_bp, url_prefix="/api/v1/admin")`
|
||||
|
||||
- [ ] T006 Generate and apply Alembic migration — dentro de `backend/` executar `alembic revision --autogenerate -m "add_page_views"`; abrir o arquivo gerado em `backend/migrations/versions/` e verificar/corrigir para que contenha: `op.create_table("page_views", sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False), sa.Column("path", sa.String(500), nullable=False), sa.Column("property_id", sa.UUID(), nullable=True), sa.Column("accessed_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), sa.Column("ip_hash", sa.String(64), nullable=False), sa.Column("user_agent", sa.String(500), nullable=True), sa.PrimaryKeyConstraint("id"))` e os dois `op.create_index`; no `downgrade` adicionar `op.drop_index` para ambos os índices antes de `op.drop_table`; executar `alembic upgrade head`
|
||||
|
||||
**Checkpoint**: Modelo registrado no ORM, tabela criada no banco, blueprint montado em `/api/v1/admin`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 — Visualizar Métricas de Acesso (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Administrador acessa `/admin/analytics`, vê cards com totais de hoje/semana/mês, seleciona período (7d/30d/90d) e visualiza gráfico de linha SVG com a série diária do período.
|
||||
|
||||
**Independent Test**: Inserir `PageView`s diretamente via fixture, chamar `GET /api/v1/admin/analytics/summary?days=30` com token admin e verificar resposta com shape correto (`today`, `this_week`, `this_month`, `period_total`, `daily_series` com exatamente 30 entradas); na UI acessar `/admin/analytics` e confirmar que cards e gráfico renderizam (com zeros se DB vazio).
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T007 [US1] Implement `GET /summary` in `backend/app/routes/analytics.py` — `@analytics_bp.get("/analytics/summary")` com `@require_admin`; ler query param `days = int(request.args.get("days", 30))`; calcular os 4 contadores via SQLAlchemy: `today` = `db.session.query(func.count(PageView.id)).filter(func.date(PageView.accessed_at) == date.today()).scalar()`, `this_week` = count com `PageView.accessed_at >= datetime.utcnow().replace(hour=0,minute=0,second=0) - timedelta(days=datetime.utcnow().weekday())`, `this_month` = count com dia 1 do mês corrente, `period_total` = count com `PageView.accessed_at >= datetime.utcnow() - timedelta(days=days)`; para `daily_series`: query `GROUP BY func.date(PageView.accessed_at)` within the period → produzir dict `{date_str: count}` → iterar `range(days-1, -1, -1)` construindo lista de `DailyPoint` (count=0 se data ausente); retornar `jsonify(AnalyticsSummary(...).model_dump())`
|
||||
|
||||
- [ ] T008 [P] [US1] Create `frontend/src/services/analytics.ts` — exportar interfaces TypeScript: `DailyPoint`, `AnalyticsSummary`, `TopPageItem`, `TopPagesResponse`, `TopPropertyItem`, `TopPropertiesResponse`; exportar função `getAnalyticsSummary(days: number): Promise<AnalyticsSummary>` implementada como `api.get<AnalyticsSummary>(\`/admin/analytics/summary?days=\${days}\`).then(r => r.data)` usando a instância `api` importada de `./api`
|
||||
|
||||
- [ ] T009 [P] [US1] Create `frontend/src/pages/admin/AdminAnalyticsPage.tsx` — estado `const [days, setDays] = useState<7|30|90>(30)` e `const [summary, setSummary] = useState<AnalyticsSummary | null>(null)` e `const [isLoading, setIsLoading] = useState(true)`; `useEffect` que chama `getAnalyticsSummary(days)` ao montar e ao trocar `days`; componente inline `PeriodFilter`: 3 botões `[7, 30, 90]` com classes Tailwind ativo=`bg-brand text-white rounded-md px-3 py-1 text-sm font-medium`, inativo=`bg-surface text-textSecondary rounded-md px-3 py-1 text-sm`; componente inline `MetricCard({ label, value })`: `div` com `bg-panel border border-borderSubtle shadow-card rounded-lg p-4`, `<p className="text-textSecondary text-sm mb-1">{label}</p>`, `<p className="text-textPrimary text-2xl font-semibold">{value}</p>`; renderizar 3 cards (Hoje, Esta Semana, Este Mês) e um card total do período; componente inline `LineChart({ series }: { series: DailyPoint[] })`: SVG `viewBox="0 0 600 120"` com `<polyline>` cujos pontos são calculados como `x = (i / (series.length-1)) * 580 + 10`, `y = 110 - (count / maxCount) * 100` (tratando `maxCount=0` como 1 para evitar NaN), `stroke="#7170ff" strokeWidth="2" fill="none"`; `<polygon>` de área preenchendo até y=110 com `fill="#5e6ad2" fillOpacity="0.1"`; labels do eixo X: renderizar data a cada 7ª posição em `<text>` com `fontSize="9" fill="#9ca3af"`; quando `maxCount === 0` exibir `<text x="300" y="65" textAnchor="middle" fill="#9ca3af" fontSize="12">Sem dados</text>` no centro; skeleton com `animate-pulse div` do Tailwind enquanto `isLoading === true`
|
||||
|
||||
- [ ] T010 [US1] Add `{ to: '/admin/analytics', label: 'Analytics' }` to `adminNavItems` array in `frontend/src/components/Navbar.tsx` — inserir como último item do array (após `{ to: '/admin/amenidades', label: 'Amenidades' }`)
|
||||
|
||||
- [ ] T011 [US1] Register route in `frontend/src/App.tsx` — adicionar `import AdminAnalyticsPage from './pages/admin/AdminAnalyticsPage'` no bloco de imports de páginas admin; adicionar `<Route path="analytics" element={<AdminAnalyticsPage />} />` dentro do bloco `<Route path="/admin" element={<AdminRoute>...}>` junto com as outras rotas admin existentes
|
||||
|
||||
**Checkpoint**: US1 totalmente funcional. Admin acessa `/admin/analytics`, vê cards e gráfico (zeros se nenhum PageView no banco). Link "Analytics" aparece na navbar do painel admin.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 — Consultar Páginas e Imóveis Mais Acessados (Priority: P2)
|
||||
|
||||
**Goal**: Administrador vê tabelas com top-10 páginas mais acessadas e top-10 imóveis mais visualizados para o período selecionado.
|
||||
|
||||
**Independent Test**: Inserir `PageView`s com paths e `property_id`s distintos; chamar `GET /api/v1/admin/analytics/top-pages?days=30` e `GET /api/v1/admin/analytics/top-properties?days=30` e verificar listas com `path+count` e `property_id+title+cover_photo+count` ordenadas DESC; confirmar que itens com `property_id` inexistente são omitidos de `/top-properties`.
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T012 [US2] Implement `GET /top-pages` in `backend/app/routes/analytics.py` — `@analytics_bp.get("/analytics/top-pages")` com `@require_admin`; ler `days`; query SQLAlchemy: `db.session.query(PageView.path, func.count(PageView.id).label("count")).filter(PageView.accessed_at >= datetime.utcnow() - timedelta(days=days)).group_by(PageView.path).order_by(func.count(PageView.id).desc()).limit(10).all()`; mapear para `list[TopPageItem]`; retornar `jsonify(TopPagesResponse(items=...).model_dump())`
|
||||
|
||||
- [ ] T013 [US2] Implement `GET /top-properties` in `backend/app/routes/analytics.py` — `@analytics_bp.get("/analytics/top-properties")` com `@require_admin`; ler `days`; query SQLAlchemy: selecionar `PageView.property_id`, `func.count(PageView.id).label("count")` WHERE `PageView.property_id != None` AND no período, `GROUP BY PageView.property_id`, `ORDER BY count DESC`, `LIMIT 10`; para cada resultado fazer `Property.query.get(row.property_id)` (pular se None — preserva comportamento de imóvel deletado); obter `cover_photo` via `PropertyPhoto.query.filter_by(property_id=prop.id).order_by(PropertyPhoto.display_order).first()`; montar `TopPropertyItem` e retornar `jsonify(TopPropertiesResponse(items=...).model_dump())`; importar `Property` de `app.models.property` e `PropertyPhoto` de `app.models.property`
|
||||
|
||||
- [ ] T014 [P] [US2] Add typed functions to `frontend/src/services/analytics.ts` — adicionar `getTopPages(days: number): Promise<TopPagesResponse>` e `getTopProperties(days: number): Promise<TopPropertiesResponse>` seguindo o mesmo padrão de `getAnalyticsSummary`
|
||||
|
||||
- [ ] T015 [US2] Add ranking tables to `frontend/src/pages/admin/AdminAnalyticsPage.tsx` — adicionar estado `const [topPages, setTopPages] = useState<TopPagesResponse | null>(null)` e `const [topProperties, setTopProperties] = useState<TopPropertiesResponse | null>(null)`; atualizar o `useEffect` para disparar os 3 fetches em paralelo via `Promise.all([getAnalyticsSummary(days), getTopPages(days), getTopProperties(days)])`; componente inline `TopPagesTable({ items })`: `table` com `bg-panel rounded-lg overflow-hidden w-full`, cabeçalho `text-textSecondary text-xs uppercase`, linhas com `text-textPrimary text-sm even:bg-surface`, colunas "Página" + "Acessos"; componente inline `TopPropertiesTable({ items })`: mesma estrutura, coluna com imagem `<img src={item.cover_photo || '/placeholder.jpg'} className="w-10 h-10 object-cover rounded" />` + título + visualizações; exibir mensagem "Sem dados para o período" quando listas estiverem vazias; renderizar as duas tabelas na página abaixo do gráfico em layout grid `grid-cols-1 lg:grid-cols-2 gap-6`
|
||||
|
||||
**Checkpoint**: US1 e US2 funcionais. Admin vê cards, gráfico e tabelas de ranking em uma única página com filtro de período unificado.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 — Rastreamento Automático de Visitas Públicas (Priority: P3)
|
||||
|
||||
**Goal**: Cada GET a uma rota pública cria um `PageView` com IP anonimizado e `property_id` quando aplicável, sem bloquear a resposta ao visitante.
|
||||
|
||||
**Independent Test**: No ambiente de teste, fazer `client.get("/api/v1/properties")` e verificar `PageView.query.count() == 1` com `path == "/api/v1/properties"` e `ip_hash` de 64 chars; fazer `client.get("/api/v1/admin/analytics/summary", headers=...)` e confirmar que o count NÃO aumenta.
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T016 [US3] Add `_track_page_view` before_request hook in `backend/app/__init__.py` — adicionar `import hashlib` e `import re` no topo do arquivo (junto aos outros imports de stdlib); dentro de `create_app()`, APÓS o registro de todos os blueprints, adicionar:
|
||||
```python
|
||||
@app.before_request
|
||||
def _track_page_view():
|
||||
try:
|
||||
if request.method != "GET":
|
||||
return
|
||||
path = request.path
|
||||
excluded = ("/api/v1/admin", "/api/v1/auth", "/static")
|
||||
if any(path.startswith(p) for p in excluded):
|
||||
return
|
||||
ip_raw = request.remote_addr or ""
|
||||
salt = current_app.config.get("IP_SALT", "")
|
||||
ip_hash = hashlib.sha256((ip_raw + salt).encode()).hexdigest()
|
||||
property_id = None
|
||||
m = re.match(r"^/api/v1/properties/([0-9a-f-]{36})$", path)
|
||||
if m:
|
||||
property_id = m.group(1)
|
||||
pv = PageView(
|
||||
path=path[:500],
|
||||
ip_hash=ip_hash,
|
||||
user_agent=(request.user_agent.string or "")[:500],
|
||||
property_id=property_id,
|
||||
)
|
||||
db.session.add(pv)
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
```
|
||||
Importar `PageView` de `app.models.page_view` no topo da função `create_app` (após os outros imports de modelo já existentes); importar `request` e `current_app` de `flask` caso não estejam já importados
|
||||
|
||||
**Checkpoint**: Rastreamento ativo. Acessar qualquer rota pública GET cria PageView no banco; `/api/v1/admin/*` e `/api/v1/auth/*` não são registrados; falha no commit não propaga exception ao visitante.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Tests & Polish
|
||||
|
||||
**Purpose**: Cobrir os 9 cenários de teste especificados no plan.md.
|
||||
|
||||
- [ ] T017 Create `backend/tests/test_analytics.py` — helper `_make_admin_token(app)` que cria um `ClientUser(role="admin")` no banco e assina um JWT com `app.config["JWT_SECRET_KEY"]`; helper `_insert_page_view(db, path, accessed_at=None, property_id=None)` para inserção direta; implementar os 9 testes:
|
||||
|
||||
**Cenário 1** — `test_summary_no_token_returns_401`: `GET /api/v1/admin/analytics/summary` sem header `Authorization` → `assert resp.status_code == 401`
|
||||
|
||||
**Cenário 2** — `test_summary_with_admin_token_returns_200_and_correct_shape`: criar admin, obter token, `GET /summary?days=7` → 200; verificar `data.keys()` contém `today, this_week, this_month, period_total, daily_series`; `len(data["daily_series"]) == 7`; todos os items de `daily_series` têm chaves `date` e `count`
|
||||
|
||||
**Cenário 3** — `test_summary_without_data_returns_all_zeros`: sem PageViews, `GET /summary?days=7` → `today == 0`, `this_week == 0`, `this_month == 0`, `period_total == 0`, todos os `count` em `daily_series` são 0
|
||||
|
||||
**Cenário 4** — `test_top_pages_respects_days_filter`: inserir 2 PageViews com `accessed_at = datetime.utcnow() - timedelta(days=60)` e 1 recente; `GET /top-pages?days=7` → somente o recente aparece no resultado
|
||||
|
||||
**Cenário 5** — `test_top_properties_omits_missing_property`: inserir PageView com `property_id = uuid4()` (imóvel inexistente); `GET /top-properties?days=30` → `data["items"] == []`
|
||||
|
||||
**Cenário 6** — `test_hook_does_not_track_admin_routes`: `GET /api/v1/admin/analytics/summary` com token admin → `PageView.query.count() == 0`
|
||||
|
||||
**Cenário 7** — `test_hook_does_not_track_auth_routes`: `POST /api/v1/auth/login` com body JSON → `PageView.query.count() == 0`
|
||||
|
||||
**Cenário 8** — `test_hook_tracks_public_get_route`: `client.get("/api/v1/properties")` → `PageView.query.count() == 1`; verificar `pv.path == "/api/v1/properties"` e `len(pv.ip_hash) == 64`
|
||||
|
||||
**Cenário 9** — `test_hook_swallows_db_exception_without_blocking_request`: usar `unittest.mock.patch("app.extensions.db.session.commit", side_effect=Exception("DB down"))`; `client.get("/api/v1/properties")` → `assert resp.status_code == 200` (a requisição não é bloqueada)
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
T001 → T002,T003,T004 (paralelo) → T005 → T006
|
||||
→ T007 [US1 backend]
|
||||
→ T008,T009 [US1 frontend, paralelo]
|
||||
T007 → T012,T013 [US2 backend]
|
||||
T008 → T014 [US2 serviço]
|
||||
T009 → T015 [US2 UI]
|
||||
T005 → T016 [US3 hook]
|
||||
T006,T007,T012,T013,T016 → T017 [Tests]
|
||||
T009 → T010,T011 [Nav + Router]
|
||||
```
|
||||
|
||||
## Parallel Execution per Phase
|
||||
|
||||
| Fase | Tarefas Paralelas |
|
||||
|------|------------------|
|
||||
| Phase 2 | T003 ∥ T004 (schemas e blueprint são arquivos distintos independentes) |
|
||||
| Phase 3 | T008 ∥ T009 (serviço frontend e página são independentes do backend e entre si) |
|
||||
| Phase 4 | T012 ∥ T013 (dois endpoints distintos no mesmo arquivo, mas sem dependência entre si); T014 ∥ T012,T013 |
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
**MVP (somente US1)** — ordem mínima para entregar o dashboard funcional com zeros:
|
||||
> T001 → T002 → T003,T004 → T005 → T006 → T007 → T008,T009 → T010 → T011
|
||||
|
||||
Com US1 completo o admin já tem uma página funcional; os dados serão zero até US3 ser implementado.
|
||||
|
||||
**Entrega incremental**:
|
||||
1. **MVP** (T001–T011): Dashboard com cards e gráfico — vazio mas sem erros
|
||||
2. **+US2** (T012–T015): Tabelas de top páginas e imóveis aparecem na mesma página
|
||||
3. **+US3** (T016): Rastreamento ativo — dados reais começam a popular o dashboard
|
||||
4. **+Tests** (T017): Cobertura dos 9 cenários de aceitação
|
||||
|
||||
**Total de tarefas**: 17
|
||||
| Fase | Tarefas |
|
||||
|------|---------|
|
||||
| Setup | 1 (T001) |
|
||||
| Foundational | 5 (T002–T006) |
|
||||
| US1 (P1) | 5 (T007–T011) |
|
||||
| US2 (P2) | 4 (T012–T015) |
|
||||
| US3 (P3) | 1 (T016) |
|
||||
| Tests & Polish | 1 (T017) |
|
||||
Loading…
Add table
Add a link
Reference in a new issue