lgcosta 2106ea14f0 ui: botões light — flat, sem gradientes/brilho/sombra e sem cantos arredondados
- .btn: fundo sólido var(--panel), borda fina, hover/active só tingem o
  fundo (panel-2 / mix com fg); removidos ::after 'vidro', text-shadow,
  inset shadows, glow, lift no hover
- .btn-primary/.btn-danger: accent/danger sólidos, hover escurece
- .segmented/.seg: container com borda única, segs justos com divisor,
  ativo em accent sólido (sem gradiente/sombra)
- select da toolbar, #btn-sidebar, .tag-chip, .tab-close, .ftree-act,
  .tree-menu button: radius 0
- sw v12
2026-11-05 11:30:28 -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 ~7 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…         │ ┌ nota.md ● ─┬─ outra.md ─┐        ✚ │ ← abas          │
│ CATEGORIAS +pasta├─────────────┴─────────────┴──────────┴────────────────┤
│ 🗂 Todas as notas│ Título da nota                       🗀 devops +1 ▾   │
│ 🗄 Sem categoria ├────────────────────────────────────────────────────────┤
│  📂 devops    1 │  1 │ # Placas                                         │
│   · 📁 k8s     2 │  2 │                                                  │
│ TAGS             │  3 │ **nvme** em `/dev/nvme0`   │ ← números de linha, │
│ #linux 2 #devops1│  4 │ - controladora #linux     │   cores, marcador   │
│ NOTAS          3 │  5 │ …                       ┊ │   de coluna (80)    │
│  nota1 .md       │                                                  │
│  nota2 .txt      │                                                  │
├─────────────────┴────────────────────────────────────────────────────────┤
│ nota.md  .md  Ln 4, Col 12      128 caracteres · 20 palavras  ● não salvo│ ← barra de status
└──────────────────────────────────────────────────────────────────────────┘

No celular (≤760px) a barra lateral vira gaveta com backdrop e o painel de versões ocupa a tela inteira; menus e toolbar rolam horizontalmente com botões só de ícone.

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 ↔ WYSIWYG ↔ Preview (Ctrl+E/M/R): o WYSIWYG (Ctrl+M) renderiza a nota num contenteditable e sincroniza com o fonte a cada tecla — Markdown vira HTML editável (negrito, títulos, listas, citação, código, links, tabelas) e volta a Markdown ao editar/alternar; HTML é editado visualmente com tags/atributos preservados (scripts e on* são removidos ao abrir). O Preview renderiza Markdown no servidor (goldmark, GFM, HTML cru escapado); HTML em <iframe sandbox>; texto puro como está. Em texto puro o WYSIWYG não se aplica.
  • 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).
    • Tecla Tab: insere N espaços (padrão 4, configurável 1–16) ou o caractere de tabulação \t; o valor também define a largura exibida do \t (tab-size) no editor e no realce, mantendo as camadas alinhadas.
    • 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 (21 opções, em grupos no diálogo): nativos — Claro (Greybird), Escuro (Adwaita), Solarizado claro/escuro; iTerm2-Color-Schemes (projeto mbadolato/iTerm2-Color-Schemes, MIT) — escuros: Dracula, Nord, Atom One Dark, Monokai Pro, Gruvbox Dark, Tokyo Night, Catppuccin Mocha, Tomorrow Night, Ayu Mirage, Kanagawa Wave; claros: Catppuccin Latte, Gruvbox Light, Atom One Light, Nord Light, Carbon Paper, Tomorrow, Ayu Light. As cores de UI e dos tokens de sintaxe são derivadas do Background/Foreground + ANSI 0–15 de cada arquivo .itermcolors; cada tema também declara color-scheme (scrollbars/controles nativos acompanham). 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).
  • Colaboração em tempo real entre dispositivos/sessões (SSE, sem recarregar página):
    • Presença: cada janela/aba do navegador é uma sessão (id aleatório em sessionStorage) que envia heartbeat das notas abertas (POST /api/presence). Quando outra sessão está com a mesma nota aberta, aparece 👥 na aba, na lista de notas e um selo "outra sessão aberta aqui" no topo do editor.
    • Propagação de salvamento: criar/salvar/restaurar/excluir publica um evento {type:"saved", note_id, hash, session} no stream SSE (GET /api/events). Instância com a nota limpa recarrega o conteúdo sozinha (toast discreto); com a nota suja, abre automaticamente o diálogo Conflito de edição (recarregar | salvar por cima). A própria sessão que salvou ignora o evento; reconexão do EventSource é automática e o fallback continua sendo a checagem no foco da janela.
  • Autosave por ociosidade: só grava após um período sem nenhuma alteração — padrão 10 s, configurável (5/10/30/60 s em Preferências); cada tecla reinicia a contagem, então digitação contínua nunca é interrompida por gravações. Silencioso (sem toast — apenas o status "salvando…/salvo"; toast continua no Ctrl+S manual). A gravação também é feita ao trocar de aba do app ou ao minimizar/trocar de janela (visibilitychange/ pagehide), para não perder edição pendente. Conflitos pausam o ciclo até a resolução.
  • Sincronia entre dispositivos (celular + desktop no mesmo servidor):
    • Cada aba lembra o hash em que se baseia (base_hash). Ao salvar, se o servidor mudou desde então, a API responde 409 e a UI abre o diálogo Conflito de edição com as duas saídas: Recarregar do servidor (descarta o texto local) ou Salvar por cima (a versão remota fica no histórico e é restaurável).
    • Abas sem edição pendente são recarregadas automaticamente quando a janela volta ao foco (visibilitychange/focus, com debounce), com aviso discreto.
  • 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 quebra em 3 linhas organizadas — ① ações de arquivo + formato + versões, ② alternador de modo em largura total, ③ ferramentas de formatação distribuídas (cabe em 393px e 320px sem corte) —, alvos de toque ≥40px, 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; dropdowns dos menus em position:fixed (o overflow-x da menubar não os corta mais) e menus rolam horizontalmente sem rótulos.
  • Botões "rich" (linguagem visual do Rich Button/spell.sh, mas em CSS puro — o componente deles é React+Tailwind, incompatível com o build-free + CSP do app): gradiente vertical com brilho de vidro, reflexo interno superior, sombra dupla e glow no primário, micro-interações de hover (eleva) / active (pressiona) / focus ring. Tudo derivado dos tokens com color-mix() + light-dark(), então os 21 temas (claros e escuros) ganham botões coerentes automaticamente; o mesmo vale para o controle segmentado Editar/WYSIWYG/Preview, selects da toolbar e chips.
  • Estilo Mousepad: tema claro Greybird-like, menus nativos (Arquivo/Editar/Exibir/Ajuda), atalhos Ctrl+S, Ctrl+N, Ctrl+E/M/R (edição/wysiwyg/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) e árvore de categorias
│   ├── diffx/            # diff unificado (gotextdiff/Myers)
│   └── api/              # rotas HTTP, auth, CRUD de categorias, 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 diretório local ./data (bind mount, ao lado do docker-compose.yml) — já ignorado pelo .gitignore. Backup = copiar a pasta; docker compose down não apaga nada.

Permissões do ./data (a imagem roda como uid 65534, non-root):

  • Podman rootless: adicione NOTAS_USER=0:0 ao .env — dentro do container isso é mapeado para o seu usuário no host, que é dono de ./data.
  • Docker (root): sudo chown -R 65534:65534 data (ou rode o container com NOTAS_USER=$(id -u):$(id -g) e ajuste a dona da pasta em conformidade).

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","base_hash":"<hash carregado>"}' \
     $B/api/notes/<id>
#     base_hash é opcional: se enviado e diferente do atual → 409 {error, server_hash, updated_at}
#     (omitir/enviar vazio = força o salvamento)
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
# tempo real (SSE): stream de eventos saved/presence (auth por cookie no navegador;
# para scripts, leia o stream com o header Bearer)
curl -N -H "Authorization: Bearer $T" "$B/api/events?session=meu-id"
# heartbeat de presença: informa quais notas a sessão tem abertas
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
     -d '{"session":"meu-id","notes":["<id>"]}' $B/api/presence
# PUT/POST/restore aceitam campo opcional "session" — o eco vai no evento saved
# (clientes que ignoram o evento podem sobrescrever a própria edição sem querer)

PWA — instalável e offline

O app é um PWA completo: instalável no Linux/Windows/macOS/Android (menu Ajuda → Instalar Aplicativo…, ou o ícone da barra de endereço), com janela standalone sem chrome do navegador e atalho de área de trabalho "Nova nota" (/?new=1).

  • web/manifest.webmanifest — name/short_name/description/start_url/scope/theme_color/ background_color, display: standalone (+ display_override com window-controls-overlay), ícones 192/512 em any, maskable e monochrome (PNGs reais em web/icons/) e atalho.
  • web/sw.js — Service Worker com cache versionado (notas-shell-v1):
    • install pré-cacheia o shell e skipWaiting; activate expurga caches antigos, clients.claim() e avisa as janelas (notas:updated → toast "App atualizado");
    • navegação: network-first com fallback ao shell (abre offline);
    • estáticos: stale-while-revalidate (carregamento instantâneo);
    • /api/* nunca é interceptado — dados sempre frescos e o SSE de tempo real não quebra.
    • Ao publicar mudanças de assets, bump na constante CACHE do sw.js.
  • Meta tags em index.html: theme-color, mobile-web-app-capable, apple-mobile-web-app-*, apple-touch-icon e <link rel="manifest">.
  • Offline UX: shell + interface funcionam sem rede; ações que precisam do servidor mostram toast claro ("Sem conexão — o app abre offline; salvar precisa do servidor").

Compatibilidade de instalação por navegador

Navegador Service Worker / offline Instalação Botão "Instalar Aplicativo…"
Chrome/Edge/Brave/Vivaldi ✅ beforeinstallprompt (prompt nativo) ✅ dispara o prompt
Firefox Android ✅ menu ⋮ → Instalar (tela inicial) ✅ abre instruções
Firefox Windows 143+ ✅ fixar na barra de tarefas pela barra de endereço ✅ abre instruções
Firefox Linux 155+ ✅ about:config → browser.taskbarTabs.enabled=true → Adicionar aba à barra de tarefas (desligado por padrão) ✅ abre instruções
Firefox macOS ✅ (app pleno em aba/offline) ainda sem fluxo nativo (em desenvolvimento na Mozilla) ✅ abre instruções honestas
Safari (macOS/iOS) ✅ manual: Compartilhar → Adicionar ao Dock/Tela de Início escondido (sem evento)

O código usa a checagem de propriedade 'onbeforeinstallprompt' in window (o evento nunca dispara em Firefox/Safari — MDN), então no Firefox o item do menu sempre aparece e mostra o passo a passo da plataforma; em standalone (app instalado) o item se esconde sozinho via display-mode: standalone, que o Firefox também reporta.

Servindo os arquivos (backend Go atual)

Já implementado em internal/api/server.go (staticHandler):

  • sw.js → Cache-Control: no-cache, no-store, must-revalidate + Content-Type: text/javascript (a detecção de update do SW é por bytes — nunca cacheie!);
  • manifest.webmanifest → Content-Type: application/manifest+json; charset=utf-8 (o Go não conhece a extensão .webmanifest por padrão);
  • tudo vem do go:embed (web/embed.go inclui manifest.webmanifest sw.js icons).
  • O Service Worker só registra em origem segura: HTTPS ou localhost/127.0.0.1 — em IP de LAN puro (http://192.168.x.x) o navegador bloqueia SW; use um túnel/reverse proxy TLS para testar instalação real fora de localhost.

Equivalente em Rust (axum, caso o backend migre)

// MIME do manifest + no-cache no SW
async fn manifest() -> impl IntoResponse {
    ([(header::CONTENT_TYPE, "application/manifest+json")], include_str!("../web/manifest.webmanifest"))
}
async fn sw() -> impl IntoResponse {
    [
        (header::CONTENT_TYPE, "text/javascript"),
        (header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"),
    ]
}

No actix-web, o fs::Files já define MIME por extensão, mas registre antes: mime::BABFILE não — use mime_guess::init::detect_EXTENSION("webmanifest", "application/manifest+json") e um wrap_fn que sobrepõe Cache-Control quando o caminho for /sw.js. (No tower-http SetResponseHeaderLayer com Match por path é o caminho limpo.)

Gerenciador de arquivos (anexos)

Árvore de pastas estilo sistema de arquivos + arquivos com associação opcional a uma nota (sem nota = avulso). Abra com Ctrl+J, menu Arquivo → Gerenciador de arquivos… ou o botão 📎 Anexo (modo seletor: clicar num arquivo insere no editor e vincula à nota).

  • Persistência (disc): data/folders.json (árvore parent), data/files.json (metadados: nome, pasta, nota, tamanho, MIME, SHA-256) e blobs em data/files/<id>.<ext> — a extensão física de tipos perigosos (html/js/svg) é neutralizada (.bin/.img); o MIME real só vive no metadado.
  • API:
    • POST /api/files/upload — multipart (file, opcionais folder_id, note_id) → 201 com url
    • GET /api/files/tree — hierarquia JSON (pastas aninhadas + arquivos + raiz)
    • GET /api/files/static/{id} — serve com MIME real, ETag (SHA-256) e Range (vídeos fazem seek), CSP: sandbox + nosniff; tipos não-inline-seguros (html/svg/js…) vão como Content-Disposition: attachment — anexar um .html malicioso não executa same-origin (stored-XSS mitigado)
    • PUT /api/files/{id}/move — {folder, note, name} (mover/associar/desvincular/renomear)
    • DELETE /api/files/{id} · POST/PUT/DELETE /api/folders[/{id}] (com anti-ciclo)
    • Limite: 64 MiB/arquivo; nomes sanitizados (filepath.Base + whitelist); ids gerados — path do disco nunca aparece na URL.
  • Editor: colar (Ctrl+V) imagem/arquivo do clipboard faz upload automático e insere ![…](url) / ![]() / link conforme o formato; arrastar-e-soltar funciona no editor e no gerenciador; no WYSIWYG a inserção é visual (<img>/ <video>) e volta a Markdown na serialização; preview no gerenciador abre lightbox com <img>/<video>/<audio>/PDF (iframe) ou texto puro.

Instruções de teste

# 1) subir o servidor e criar uma nota pela UI (ou curl)
export T=seu-token; B=http://127.0.0.1:8080
# 2) upload avulso e em pasta
FID=$(curl -s -X POST $B/api/files/folders -H "Authorization: Bearer $T" \
  -H 'Content-Type: application/json' -d '{"name":"docs","parent":""}' | jq -r .id)
curl -s -X POST $B/api/files/upload -H "Authorization: Bearer $T" \
  -F file=@foto.png -F folder_id=$FID | jq .
# 3) árvore + estático com Range/headers
curl -s $B/api/files/tree -H "Authorization: Bearer $T" | jq
curl -sI -H "Authorization: Bearer $T" -H 'Range: bytes=0-9' $B/api/files/static/<id>
# 4) XSS guard: anexar .html deve vir attachment
printf '<script>alert(1)</script>' > x.html
curl -s -X POST $B/api/files/upload -H "Authorization: Bearer $T" -F file=@x.html | jq -r .id
curl -sI -H "Authorization: Bearer $T" $B/api/files/static/<id> | grep -i disposition
# 5) UI: Ctrl+J abre o gerenciador; cole uma imagem (Ctrl+V) no editor e arraste
#    arquivos para dentro dele; use 📎 para inserir um arquivo existente.

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.
  • Preferências do editor e abas abertas ficam em localStorage (dados não sensíveis, por dispositivo/navegador); o token nunca vai para lá.
  • Presença/SSE: o stream exige a mesma autenticação das rotas /api/* (cookie HttpOnly no navegador, Bearer em scripts). O registro de presença guarda apenas id de sessão aleatório + ids de notas abertas — nada de conteúdo trafega no canal de presença.

Testes

go test ./...   # store (revisões/GC/tags/árvore de categorias/migração),
                # diffx (diff unificado), api (fluxo HTTP completo + CRUD de categorias)
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%