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

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)

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

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.

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

go test ./...   # store (revisões/GC/tags), diffx (diff unificado), api (fluxo HTTP completo)
go vet ./...
node --check web/js/app.js
S
Description
Gerenciador de notas web estilo Mousepad (Go + frontend embutido): categorias, tags #tag, txt/md/html, preview, histórico de revisões estilo Git com diff e restauração, auth por token.
Readme
945 KiB
0 Stars 1 Watchers 0 Forks
Languages
Go 40.5%
JavaScript 37.8%
CSS 16.5%
HTML 4.9%
Dockerfile 0.3%