1. /api/render: cap de 512 KiB na entrada (amplificação ~2× medida);
decodeJSON agora amarra http.MaxBytesReader ao ResponseWriter (413
real ao cliente em todos os endpoints JSON)
2. Login com rate-limit por IP em memória (5 falhas/15 min → 429, acerto
reseta, RemoteAddr puro — X-F-F não é confiável) + log de falha com IP
3. Upload: MaxBytesReader no corpo inteiro (64 MiB + 64 KiB) antes do
parse do multipart; quota global de 2 GiB (MaxFilesTotalBytes) em AddFile
4. sanitizeRich neutraliza href/src com javascript:/vbscript:/data:
(case/whitespace-insensível); link legítimo preservado
5. Cookie Secure passa a ser configuração explícita (NOTES_SECURE_COOKIE)
— X-Forwarded-Proto é spoofável; documentado em .env.example e compose
6. Cookie agora carrega id de sessão opaco (crypto/rand 32B, TTL 30d,
revogação no logout e no restart); token mestre só vale via Bearer —
furo pego pelo próprio teste novo (token no cookie era aceito)
Testes: rate-limit, revogação, canais separados, render 413, upload cap
sw v16
- Abas: HTML5 drag-and-drop — arrastar uma aba sobre outra mostra marca de
inserção (borda accent à esquerda/direita conforme o lado) e solta reordenando;
soltar na área vazia da aba-barra move para o fim; ordem persiste em
localStorage (passa a valer após o debounce de 400ms de persistTabs);
✕ da aba não inicia arraste (dragstart cancelado)
- Toolbar: botão 🗂 Arquivos abre o gerenciador no modo normal (não seletor),
equivalente ao Ctrl+J
- Lista de notas (sidebar): botão 🗑 em cada linha abre o mesmo diálogo de
confirmação do menu e exclui a nota mesmo sem aba aberta; se houver aba da
nota, ela é fechada e a aba vizinha ativada; no mobile o botão fica sempre
visível (alvo 32px, sem depender de hover)
- deleteNote() agora delega para deleteNoteById(n) (reuso do fluxo com
presença/persistTabs)
- sw v15
Dois defeitos de geometria entre o textarea transparente (desenha caret e
seleção) e o overlay #hl (desenha o texto visível):
1. Largura: no Windows/Chrome o scrollbar clássico do textarea (~15-17px)
reduz sua área de texto; o overlay (overflow:hidden) não — cada linha
quebrava em colunas diferentes e a seleção ficava com 'buracos' e
deslocamento progressivo. Agora: scrollbar-gutter:stable no textarea
(largura constante, sem saltar ao cruzar 1 tela) + --sbw medido por JS
espelhado no padding-right do overlay a cada render.
2. Negrito sintetizado: tk-h/tk-strong/tk-mark usavam font-weight 700; sem
face bold na fonte monoespaçada, o Chrome alonga os glifos (~1-2px/char)
e o overlay acumulava offset por linha (visível no print: seleção parava
no fim do trecho negrito). font-synthesis:none no overlay + pseudo-negrito
por text-shadow (.35px) — peso visual preservado, avanço idêntico.
Também: renderEditorVisual reassincroniza scrollTop/scrollLeft de hl/gutter
após replaceChildren (antes o Enter em nota longa zerava o scroll do overlay
— 'Enter não atualiza a visualização').
Verificado headless: com scrollbar clássico simulado (15px), largura de texto
textarea==overlay (1078==1078), seleção contínua até o fim correto da linha
quebrada; Enter com doc rolado mantém ta/hl/gutter em 5000; wysiwyg/preview
sem regressão. sw v14.
Firefox calcula line-height:normal pelas métricas verticais da fonte do
conteúdo (Noto Color Emoji infla botões com ícone; glifos simples ficam
menores) → linha desalinhada/desproporcional, igual em Chrome.
- .btn: inline-flex + height 28px (small 22px) — conteúdo não afeta a caixa
- .segmented .seg: height 26px (container 28 com a borda); select da toolbar 28px
- .tag-chip 20px; .tree-menu/.ftree-act line-height 1
- resets ::-moz-focus-inner/-outer
- sw v13
Verificado com Firefox 159 headless e Chromium no mesmo harness e no app
real: alturas idênticas (28/26/22/20 desktop; 40/40/38 mobile via min-height).
Sistema de botões (mesma linguagem do Rich Button do spell.sh — que é
React/Tailwind e não se aplica a um app vanilla com CSP script-src 'self'):
- gradiente vertical com gloss de vidro (::after), borda e sombra derivadas
dos tokens, lift no hover, press no active, focus ring, glow no primário
- variantes primary/danger/icon/small, selects e segmented control ricos
- 100% via color-mix() + light-dark() sobre os tokens: adapta-se sozinho aos
21 temas (color-scheme declarado em todos, incl. nativos agora)
- cores antes fixas (#cfe2fa/#e9e6e2/#dcd7d2/#dbe9fb/#e5effb) tokenizadas
Toolbar responsiva (≤760px):
- fim do scroll horizontal: quebra em 3 linhas — arquivo+formato+versões /
alternador de modo full-width / ferramentas de formatação full-width com
flex-wrap de segurança; alvos ≥40px
- verificado em 393px (última borda 385) e 320px (311), uma linha de ícones
sw.js: CACHE notas-shell-v8; README atualizado
- firefoxInstallHelp(): passo a passo específico para Linux — about:config →
browser.taskbarTabs.enabled=true → 'Adicionar aba à barra de tarefas';
macOS separado (ainda sem fluxo nativo)
- README: matriz de compatibilidade detalha Linux 155+ e macOS
- sw.js: CACHE notas-shell-v4
Erro relatado: 'can't access property "key", opened[want] is undefined'.
Causa: persistTabs gravava active = -1 quando findIndex não achava a aba
ativa (corrida em pagehide/fechamento); restoreTabs usava o índice cru →
opened[-1] undefined derrubava o boot inteiro.
- persistTabs: grava Math.max(0, index) — nunca negativo
- restoreTabs: clamp do índice salvo para [0, len-1]
- boot: try/catch em restoreTabs com fallback de rascunho novo — estado
corrompido algum (JSON lixo, ids fantasmas, índices inválidos) não impede
mais o app de abrir
- sw.js: CACHE bump para notas-shell-v3
Testado: {open:[{id:fantasma}],active:-1} e {active:5} com rascunho → boot
limpo, rascunho restaurado, sem toast de erro.
- Detecção por propriedade ('onbeforeinstallprompt' in window) — evento é
exclusivo de Chromium; Firefox/Safari nunca o disparam (nem 'appinstalled')
- Firefox: item 'Instalar Aplicativo…' sempre visível (fora de standalone) e
abre modal com instruções por plataforma — Android (menu ⋮ → Instalar),
Windows 143+ (fixar na barra de tarefas), Linux/macOS (sem instalação
nativa; app pleno em aba/offline)
- Escondido em standalone via display-mode (Firefox também o reporta)
- sw.js: bump CACHE para notas-shell-v2 (shell novo)
- README: matriz de compatibilidade por navegador
- Testado: Chromium (prompt real), Firefox-Win UA (modal taskbar),
Firefox-Android UA (modal menu), Chromium sem BIP (item escondido)
- manifest.webmanifest: standalone, theme/background color, ícones 192/512
any+maskable+monochrome (PNGs em web/icons) e atalho 'Nova nota' (?new=1)
- sw.js: cache versionado (notas-shell-v1); navegação network-first com
fallback ao shell (offline), estáticos stale-while-revalidate, /api/* e
SSE sempre passthrough; activate limpa caches e notifica janelas
- app.js: registro do SW, beforeinstallprompt com item 'Instalar Aplicativo…'
no menu Ajuda (escondido em standalone/após instalar), toast de atualização,
aviso offline/online, deep-link ?new=1
- index.html: meta tags theme-color/mobile-web-app/apple-touch-icon/manifest
- server: staticHandler com MIME application/manifest+json e
Cache-Control no-cache para /sw.js; embed inclui manifest, sw e ícones
- README: seção PWA + orientação de serving (Go atual e equivalente Rust)