Files
notas-online/README.md
T
lgcosta 7e89207b70 feat: layout mobile, abas múltiplas e categorias em árvore
- Responsivo ≤760px: sidebar vira gaveta com backdrop, painel de versões em tela
  cheia, toolbar/menu rolam sem rótulos, alvos de toque, dvh + safe-area, fonte
  do editor ≥16px (anti-zoom iOS)
- Abas: várias notas abertas com estado sujo por aba, ✕ com confirmação,
  clique do meio, Alt+W / Alt+←/→ / Alt+1..9; abas e rascunhos sobrevivem à
  sessão via localStorage
- Categorias: modelo vira árvore (categories.json) com notas em múltiplas
  categorias; explorador no sidebar com expandir/renomear/mover/excluir
  (sem ciclos, GC de vínculos) e seletor multi-marca no editor; filtro por
  pasta recursivo por padrão; migração automática do campo único legado
- API: POST/PUT/DELETE /api/categories; notas aceitam categories: [ids]
- Testes: store (árvore, ciclo, migração, GC) e api (CRUD de categorias)
2026-11-04 11:54:15 -03:00

193 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Notas — editor de notas web estilo Mousepad
Aplicação web completa para gerenciamento de notas inspirada no visual sóbrio e minimalista do
**Mousepad** (editor de texto do XFCE): barra de menus superior, barra de ferramentas, área de
texto limpa e barra de status na parte inferior.
Backend único em **Go** (binário estático de ~11 MB com o frontend já embutido), armazenamento em
disco no estilo **Git** (blobs endereçados por SHA-256 + histórico de revisões com diff e
restauração) e autenticação por **token** (Bearer para API / cookie HttpOnly para o navegador).
```
┌──────────────────────────────────────────────────────────────────────┐
│ ☰ Notas Arquivo Editar Exibir Ajuda │ ← barra de menus
├──────────────────────────────────────────────────────────────────────┤
│ +Nova 💾Salvar ⬇Baixar │ B I S H1 H2 • 1 ❝ 🔗 </> { } │ Formato ▾ │ ← barra de ferramentas
│ [ Editar | Preview ] 🕘Versões │
├──────────────┬──────────────────────────────────────┬────────────────
│ Buscar… │ Título da nota categoria │ HISTÓRICO DE │
│ CATEGORIA ▾ ├──────────────────────────────────────┤ │ REVISÕES │
│ TAGS │ # Placas │ │ hash · autor │
│ #linux 2 │ **nvme** em `/dev/nvme0` │ │ mensagem · data│
│ NOTAS │ - controladora #linux │ ├────────────────┤
│ nota1 .md │ … │ │ diff unificado │
│ nota2 .txt │ │ │ +restaurar │
├──────────────┴──────────────────────────────────────┴────────────────┤
│ nota.md .md Ln 4, Col 12 128 caracteres · 20 palavras ●● │ ← barra de status
└──────────────────────────────────────────────────────────────────────┘
```
## Funcionalidades
- **3 formatos por nota**: Texto puro (`.txt`), Markdown (`.md`) e HTML (`.html`), alternáveis no
editor; a nota é baixada com a extensão correspondente.
- **Barra de ferramentas de formatação**: negrito, itálico, tachado, títulos H1/H2, listas,
citação, link, código inline e bloco de código — inserindo sintaxe Markdown **ou** HTML conforme
o formato ativo (desabilitada em texto puro).
- **Modo Edição ↔ Preview**: Markdown é renderizado no servidor (goldmark, GFM, HTML cru escapado);
HTML é pré-visualizado em `<iframe sandbox>`; texto puro é exibido como está.
- **Categorias como árvore de pastas**: o painel lateral traz um explorador de arquivos —
categorias aninhadas (subpastas), expansão/colapso, contagem de notas por pasta e menu de
contexto (Nova subpasta / Renomear / Excluir, sem ciclos). Uma nota pode pertencer a
**várias categorias** ao mesmo tempo (seletor multi-marca no topo do editor). Filtro por
pasta é recursivo por padrão (`?recursive=0` desliga); há também "Todas as notas" e
"Sem categoria". Dados antigos com o campo único `category` são migrados automaticamente
na abertura (vira categoria raiz de mesmo nome).
- **Tags `#tag`** extraídas automaticamente do conteúdo (cabeçalhos Markdown como `# Título`
não contam — só `#tag` sem espaço), com filtro por nuvem de tags e busca em
título/categorias/conteúdo.
- **Abas de arquivos**: abra várias notas ao mesmo tempo (clique na lista, `Ctrl+N` ou botão
✚); cada aba tem estado sujo próprio (●), botão ✕ com confirmação, fechar com clique do
meio, trocar com `Alt+←/→` ou `Alt+1..9`, fechar com `Alt+W`. As abas abertas (inclusive
rascunhos não salvos) são restauradas entre sessões via `localStorage`.
- **Preferências do editor** (menu Editar → Preferências… ou `Ctrl+,`, em diálogo próprio):
- **Números de linha** (gutter alinhado mesmo com quebra de linha; alternável pelo menu Exibir).
- **Marcador de coluna** customizável — padrão **80** (linha vertical no editor, exibida com
quebra de linha desligada, como no Mousepad/gedit).
- **Quebrar linha** ligado/desligável (alternável também pelo menu Exibir).
- **Fonte** (DejaVu Sans Mono, Ubuntu Mono, Cascadia Code, Liberation Mono, Courier New ou
monoespaçada do sistema) e **tamanho** (10–32 px).
- **Esquema de cores / tema**: Claro (Greybird), Escuro (Adwaita), Solarizado claro e
Solarizado escuro — aplicam-se a toda a interface, incluindo preview e diffs.
- **Colorização de sintaxe**: Markdown (títulos, negrito/itálico, código inline e blocos ` ``` `,
listas, citações, links, tags `#tag`, URLs) e HTML (tags, atributos, strings, comentários,
entidades) renderizados por overlay sobre o textarea; desligável.
- Tudo persistido no navegador (`localStorage`), com pré-visualização ao vivo no diálogo,
botões Cancelar (reverte) e Restaurar padrões.
- **Versionamento estilo Git**: cada salvamento de conteúdo cria uma revisão imutável
(autor, data, mensagem, hash SHA-256 e parent). O painel de histórico mostra o **diff unificado**
de cada revisão e permite **restaurar** qualquer versão anterior (a restauração vira uma nova
revisão — nada se perde). Conteúdo idêntico é deduplicado (content-addressed).
- **Autenticação por token**: sem token, a interface exibe um diálogo pedindo o token; ele é
guardado em **cookie HttpOnly/SameSite=Lax** (30 dias). A API também aceita
`Authorization: Bearer <token>` para scripts.
- **Responsivo para celular** (≤760px): barra lateral vira gaveta com backdrop (☰ fecha ao
tocar uma nota), painel de versões ocupa a tela toda, toolbar e menus rolam horizontalmente
sem rótulos, alvos de toque ampliados, diálogos em largura total, fonte do editor com mínimo
de 16px (evita zoom automático do iOS), altura em `dvh` e `safe-area-inset` para notch.
- **Estilo Mousepad**: tema claro Greybird-like, menus nativos (Arquivo/Editar/Exibir/Ajuda),
atalhos `Ctrl+S`, `Ctrl+N`, `Ctrl+E/R` (editar/preview), `Ctrl+H` (histórico), `F9` (barra
lateral), `Ctrl+B/I/K` (formatação), `Ctrl+,` (preferências), `Alt+W`/`Alt+←/→`/`Alt+1..9`
(abas), barra de status com arquivo, formato, linha/coluna, contagem de palavras e estado de
salvamento.
## Estrutura do projeto
```
.
├── cmd/notas/ # main: flags, env, graceful shutdown
├── internal/
│ ├── store/ # persistência em disco + revisões (estilo Git)
│ ├── diffx/ # diff unificado (gotextdiff/Myers)
│ └── api/ # rotas HTTP, auth, render de Markdown
├── web/ # frontend estático (go:embed no binário)
│ ├── index.html css/app.css js/app.js
├── Dockerfile # multi-stage: golang:alpine → scratch
├── docker-compose.yml
└── .env.example
```
## Como rodar com Docker (recomendado)
```bash
cp .env.example .env
# edite .env e defina o token (gere um forte):
# openssl rand -hex 24
docker compose up -d
# abra http://localhost:8080 e informe o token na tela de login
```
Se a porta 8080 estiver ocupada: `NOTAS_PORT=9090 docker compose up -d`.
Com **Podman**: tudo acima funciona igualmente com `podman-compose`.
Os dados ficam no volume nomeado `notas-data` — `docker compose down` não os apaga
(não use `down -v` se quiser preservá-los). Faça backup copiando o volume.
## Como rodar sem Docker
```bash
export NOTES_TOKEN=$(openssl rand -hex 24)
go run ./cmd/notas # escuta :8080, dados em ./data
# ou o binário:
go build -trimpath -ldflags="-s -w" -o notas ./cmd/notas
./notas -addr :8080 -data ./data
```
### Configuração (variáveis de ambiente / flags)
| Variável | Flag | Padrão | Descrição |
|------------------|----------|-----------|--------------------------------------------|
| `NOTES_TOKEN` | — | (sem) | **Obrigatório.** Token de acesso. |
| `NOTES_DATA_DIR` | `-data` | `./data` | Diretório de dados (notas + blobs). |
| `NOTES_ADDR` | `-addr` | `:8080` | Endereço de escuta. |
## API (para scripts)
Todos os endpoints sob `/api/*` exigem o token, exceto `login/logout/me` e o frontend.
```bash
T=$NOTES_TOKEN; B=http://localhost:8080
curl -H "Authorization: Bearer $T" $B/api/notes # lista (?category=<id|none>&recursive=0&tag=&q=)
curl -H "Authorization: Bearer $T" $B/api/notes/<id> # nota + conteúdo
curl -s -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"# Oi #linux"}' $B/api/notes
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"editado","author":"lg","message":"revisão 2"}' \
$B/api/notes/<id>
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions # histórico
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions/<hash>/diff # diff unificado
curl -X POST -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions/<hash>/restore \
-H 'Content-Type: application/json' -d '{"author":"lg"}' # restaurar
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/raw # download .txt/.md/.html
curl -H "Authorization: Bearer $T" $B/api/tags $B/api/categories # agregações
# árvore de categorias (lista plana; parent="" é raiz)
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"devops","parent":""}' $B/api/categories
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"infra","parent":"<novo-pai>"}' $B/api/categories/<id> # renomear/mover
curl -X DELETE -H "Authorization: Bearer $T" $B/api/categories/<id> # remove subtree
```
## Como funciona o versionamento
- O conteúdo é guardado em `data/blobs/<aa>/<hash-sha256>` (content-addressed, como objetos do
Git): conteúdo repetido ocupa um único blob.
- Cada nota tem `data/notes/<id>/meta.json` (estado atual) e `history.json` (lista ordenada de
revisões: `hash`, `parent`, `author`, `when`, `message`, `bytes`).
- A árvore de categorias fica em `data/categories.json` (lista plana com `parent`; nomes únicos
entre irmãos; mover para dentro de si mesma é recusado).
- Salvar sem alterar o conteúdo atualiza apenas os metadados (título/categorias/formato) — sem
revisão nova. Restaurar cria uma revisão nova apontando para o blob antigo.
- Apagar uma nota faz GC dos blobs que ficaram sem referência.
## Segurança
- Token verificado em **tempo constante**; nunca é registrado nos logs.
- Sessão do navegador em cookie **HttpOnly + SameSite=Lax** (mitiga CSRF); o token em
`localStorage` foi preterido de propósito por ser exposto a XSS.
- CSP restritiva (`default-src 'self'`), `X-Frame-Options: DENY`, `nosniff`; preview de HTML roda
em iframe `sandbox` sem mesma origem; Markdown renderiza com HTML cru escapado.
- Limite de 2 MiB por nota; corpo HTTP limitado a 4 MiB.
- Rode atrás de TLS (reverse proxy) em produção — o cookie marca `Secure` automaticamente quando
a requisição chega por HTTPS/`X-Forwarded-Proto`. O modelo é **token único compartilhado**
(sem usuários múltiplos); a imagem final é `scratch` non-root (uid 65534) — sem shell para
`exec` de depuração, por design.
## Testes
```bash
go test ./... # store (revisões/GC/tags), diffx (diff unificado), api (fluxo HTTP completo)
go vet ./...
node --check web/js/app.js
```