feat: aplicação inicial — gerenciador de notas web estilo Mousepad

- Backend Go: API REST, auth por token (Bearer + cookie HttpOnly), storage em disco
  com revisões content-addressed (estilo Git: hash, autor, data, diff, restauração)
- Editor txt/md/html com preview, tags #tag, categorias, painel de histórico
- Preferências: números de linha, marcador de coluna (padrão 80), quebra de linha,
  fonte/tamanho, 4 temas e colorização de sintaxe (overlay no textarea)
- Frontend vanilla embutido via go:embed; Dockerfile multi-stage + docker-compose
This commit is contained in:
lgcosta committed 2026-11-04 09:41:46 -03:00
commit 9bc5de4c56
22 files changed
+4239

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
# 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/pastas** no painel lateral e **tags `#tag`** extraídas automaticamente do conteúdo
(cabeçalhos Markdown como `# Título` não contam — só `#tag` sem espaço). Filtro por categoria,
por tag e busca em título/categoria/conteúdo.
- **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.
- **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), 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 (filtros: ?category=&tag=&q=)
curl -H "Authorization: Bearer $T" $B/api/notes/<id> # nota + conteúdo
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","category":"dev","content":"# Oi #linux"}' $B/api/notes
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","category":"dev","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
```
## 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`).
- Salvar sem alterar o conteúdo atualiza apenas os metadados (título/categoria/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
```