docs: plano das secoes escondidas na tela de log
This commit is contained in:
@@ -0,0 +1,235 @@
|
|||||||
|
# Log: Seções Escondidas por Padrão — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Fazer as duas seções da tela `/log` ("Ações no painel" e "Envios
|
||||||
|
automáticos de chave (n8n)") começarem fechadas ao carregar a página,
|
||||||
|
exigindo um clique no título ou em um botão "Mostrar" para expandir.
|
||||||
|
|
||||||
|
**Architecture:** Envolver cada seção em `<details class="secao-log">`
|
||||||
|
nativo do HTML (fechado por padrão, sem `open`), com um `<summary>` que
|
||||||
|
contém o título e um botão `Mostrar`/`Ocultar`. Um script inline pequeno
|
||||||
|
sincroniza o texto do botão e `aria-expanded` com o evento `toggle` nativo
|
||||||
|
do `<details>` — a abertura/fechamento em si não precisa de JavaScript,
|
||||||
|
só o texto do botão.
|
||||||
|
|
||||||
|
**Tech Stack:** Jinja2 (`app/templates/log.html`), CSS puro
|
||||||
|
(`app/static/estilo.css`), JavaScript vanilla inline, pytest +
|
||||||
|
`fastapi.testclient.TestClient` (`tests/test_router_log.py`).
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Nenhuma seção pode abrir automaticamente ao carregar a página (spec:
|
||||||
|
"Fora de escopo" — abrir automaticamente qualquer seção por padrão).
|
||||||
|
- Estado aberto/fechado não é persistido entre recarregamentos (sem
|
||||||
|
localStorage, cookie ou parâmetro de URL).
|
||||||
|
- Nenhum dado exibido, paginação, limite de 20 registros em "Ações no
|
||||||
|
painel" ou o botão "Limpar log" (visível só para admin) pode mudar de
|
||||||
|
comportamento — só o wrapper de exibição muda.
|
||||||
|
- A ordem das seções continua: "Ações no painel" primeiro, "Envios
|
||||||
|
automáticos de chave (n8n)" embaixo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Seções `<details>` no template + estilo + teste
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/templates/log.html`
|
||||||
|
- Modify: `app/static/estilo.css`
|
||||||
|
- Test: `tests/test_router_log.py`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: contexto já passado pela rota `GET /log` em
|
||||||
|
`app/routers/log.py` (`acoes`, `envios`, `envios_configurado`, `eh_admin`,
|
||||||
|
`csrf_token`, `mensagem`, `mensagem_tipo`) — nenhuma mudança nesse router
|
||||||
|
nesta tarefa.
|
||||||
|
- Produces: nada consumido por outras tasks (task única).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Escrever o teste que falha**
|
||||||
|
|
||||||
|
Adicionar ao final de `tests/test_router_log.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_pagina_log_secoes_comecam_fechadas(monkeypatch):
|
||||||
|
monkeypatch.setattr(router_log.db_log, "listar", lambda limite=20: [])
|
||||||
|
monkeypatch.setattr(router_log.n8n_client, "listar_envios", lambda limite=50: [])
|
||||||
|
monkeypatch.setattr(router_log.n8n_client, "configurado", lambda: False)
|
||||||
|
|
||||||
|
resposta = client.get("/log")
|
||||||
|
assert resposta.status_code == 200
|
||||||
|
texto = resposta.text
|
||||||
|
assert texto.count('<details class="secao-log">') == 2
|
||||||
|
assert "<details class=\"secao-log\" open>" not in texto
|
||||||
|
assert "<details class='secao-log' open>" not in texto
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Rodar o teste para confirmar que falha**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_router_log.py::test_pagina_log_secoes_comecam_fechadas -v`
|
||||||
|
Expected: FAIL — o HTML atual não tem `<details class="secao-log">`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Reescrever `app/templates/log.html`**
|
||||||
|
|
||||||
|
Substituir o conteúdo do bloco `{% block conteudo %}` inteiro por:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% block conteudo %}
|
||||||
|
<h1>Log</h1>
|
||||||
|
|
||||||
|
<details class="secao-log">
|
||||||
|
<summary>
|
||||||
|
<span class="secao-log-titulo">Ações no painel</span>
|
||||||
|
<button type="button" class="botao-toggle-log" aria-expanded="false">Mostrar</button>
|
||||||
|
</summary>
|
||||||
|
<div class="secao-log-conteudo">
|
||||||
|
{% if eh_admin %}
|
||||||
|
<form method="post" action="/log/limpar" style="margin-bottom:14px">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<button type="submit" onclick="return confirm('Limpar todo o histórico de ações? Isso não pode ser desfeito.')">Limpar log</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if acoes %}
|
||||||
|
<div class="tabela-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>Quando</th><th>Usuário</th><th>Ação</th><th>Entidade</th><th>Status</th><th>Detalhe</th></tr>
|
||||||
|
{% for a in acoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.CRIADO_EM }}</td>
|
||||||
|
<td>{{ a.USUARIO }}</td>
|
||||||
|
<td>{{ a.ACAO }}</td>
|
||||||
|
<td>{{ a.ENTIDADE }}{% if a.ENTIDADE_ID %} #{{ a.ENTIDADE_ID }}{% endif %}</td>
|
||||||
|
<td><span class="badge {{ 'badge-sucesso' if a.SUCESSO else 'badge-neutro' }}">{{ "OK" if a.SUCESSO else "ERRO" }}</span></td>
|
||||||
|
<td>{{ a.DETALHE or "" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>Nenhuma ação registrada ainda.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="secao-log">
|
||||||
|
<summary>
|
||||||
|
<span class="secao-log-titulo">Envios automáticos de chave (n8n)</span>
|
||||||
|
<button type="button" class="botao-toggle-log" aria-expanded="false">Mostrar</button>
|
||||||
|
</summary>
|
||||||
|
<div class="secao-log-conteudo">
|
||||||
|
{% if not envios_configurado %}
|
||||||
|
<div class="cartao">
|
||||||
|
<p>
|
||||||
|
Integração com o n8n não configurada — defina <code>N8N_API_URL</code> e
|
||||||
|
<code>N8N_API_KEY</code> no ambiente (ver <code>.env.example</code>) para ver
|
||||||
|
aqui os envios automáticos de chave feitos pelo workflow do n8n.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% elif not envios %}
|
||||||
|
<p>Nenhum envio encontrado (ou não foi possível consultar o n8n agora).</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="tabela-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th><th>Cliente</th><th>Código</th><th>Competência</th><th>Status</th>
|
||||||
|
<th>Telefones enviados</th><th>Telefones com erro</th>
|
||||||
|
</tr>
|
||||||
|
{% for e in envios %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ e.data_processamento }}</td>
|
||||||
|
<td>{{ e.nome_cliente }}</td>
|
||||||
|
<td>{{ e.codigo_cliente }}</td>
|
||||||
|
<td>{{ e.competencia }}</td>
|
||||||
|
<td>{{ e.status }}</td>
|
||||||
|
<td>{{ e.telefones_enviados }}</td>
|
||||||
|
<td>{{ e.telefones_com_erro or "" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll(".secao-log").forEach(function (secao) {
|
||||||
|
var botao = secao.querySelector(".botao-toggle-log");
|
||||||
|
var atualizar = function () {
|
||||||
|
var aberto = secao.hasAttribute("open");
|
||||||
|
botao.textContent = aberto ? "Ocultar" : "Mostrar";
|
||||||
|
botao.setAttribute("aria-expanded", aberto ? "true" : "false");
|
||||||
|
};
|
||||||
|
secao.addEventListener("toggle", atualizar);
|
||||||
|
atualizar();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notas importantes:
|
||||||
|
- O `<button>` fica dentro do `<summary>`: clicar nele já aciona o toggle
|
||||||
|
nativo do `<details>` (o clique borbulha até o `<summary>`), então não
|
||||||
|
precisa de `onclick` para abrir/fechar — só o listener do evento
|
||||||
|
`toggle` para trocar o texto.
|
||||||
|
- Nenhum `<details>` tem o atributo `open` — as duas começam fechadas.
|
||||||
|
- O botão "Limpar log" (dentro de "Ações no painel") continua exatamente
|
||||||
|
igual, só mudou de lugar (dentro do `<div class="secao-log-conteudo">`).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Adicionar estilo em `app/static/estilo.css`**
|
||||||
|
|
||||||
|
Adicionar ao final do arquivo:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.secao-log { margin-bottom: 20px; }
|
||||||
|
.secao-log summary {
|
||||||
|
display: flex; align-items: center; gap: 10px; cursor: pointer;
|
||||||
|
list-style: none; padding: 10px 0; user-select: none;
|
||||||
|
}
|
||||||
|
.secao-log summary::-webkit-details-marker { display: none; }
|
||||||
|
.secao-log summary::before {
|
||||||
|
content: "▸"; display: inline-block; color: var(--cor-texto-secundario);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.secao-log[open] summary::before { transform: rotate(90deg); }
|
||||||
|
.secao-log-titulo { font-size: 1.17em; font-weight: 700; }
|
||||||
|
.botao-toggle-log {
|
||||||
|
margin-top: 0; padding: 6px 12px; font-size: 0.85rem; font-weight: 400;
|
||||||
|
background: transparent; border: 1px solid var(--cor-borda); color: var(--cor-texto);
|
||||||
|
}
|
||||||
|
.botao-toggle-log:hover { border-color: var(--cor-acento); color: var(--cor-acento); }
|
||||||
|
.secao-log-conteudo { padding-top: 4px; }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rodar o teste para confirmar que passa**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_router_log.py -v`
|
||||||
|
Expected: PASS em todos os testes do arquivo (incluindo os 6 já existentes
|
||||||
|
e o novo).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Rodar a suíte completa**
|
||||||
|
|
||||||
|
Run: `pytest`
|
||||||
|
Expected: PASS em todos os testes do projeto (nenhum outro teste referencia
|
||||||
|
a estrutura HTML de `log.html`, então nada mais deve quebrar).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/templates/log.html app/static/estilo.css tests/test_router_log.py
|
||||||
|
git commit -m "feat: secoes da tela de log comecam fechadas, com clique para abrir"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **Spec coverage:** as duas seções ficam em `<details>` fechado (spec:
|
||||||
|
"estrutura"); título clicável com seta (spec: "controles — título");
|
||||||
|
botão Mostrar/Ocultar dentro do summary, toggle nativo sem duplicar lógica
|
||||||
|
(spec: "controles — botão"); nenhum estado persistido (spec: "estado não
|
||||||
|
é persistido"); ordem mantida; nenhuma mudança em dados/paginação/limite/
|
||||||
|
botão limpar (spec: "sem mudança de comportamento"). Teste cobre que as
|
||||||
|
duas seções carregam sem `open`.
|
||||||
|
- **Placeholder scan:** nenhum "TBD"/"implementar depois" — todo HTML, CSS
|
||||||
|
e JS estão completos e prontos para colar.
|
||||||
|
- **Type consistency:** não há funções/assinaturas cruzando tasks (task
|
||||||
|
única, sem mudança de router).
|
||||||
Reference in New Issue
Block a user