Skip to content

Lixeira, aplicativos do Win95 e correção de dois bugs de concorrência - #1

Merged
renanlocatizfernandes merged 19 commits into
masterfrom
feat/lixeira-e-melhorias
Aug 21, 2026
Merged

renanlocatizfernandes merged 19 commits into
masterfrom
feat/lixeira-e-melhorias

Conversation

@renanlocatizfernandes

@renanlocatizfernandes renanlocatizfernandes commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Dezenove commits. Seis agentes em paralelo. Tudo verificado contra o stack Docker e reverificado no CI.

Dois bugs reais, achados por agentes

Race de position no POST (3c11221). As rotas liam a maior posição e inseriam depois, sem atomicidade. Quinze criações simultâneas produziam 5 posições distintas. O efeito não é cosmético: com dois cards empatados, positionFor() calcula (a+b)/2 === a para o slot entre eles — o servidor responde 200 OK e o card não sai do lugar. O desempate por id no orderBy mantinha a ordem estável, então nada parecia quebrado; o drag simplesmente parava de funcionar.

Transação do Prisma não resolveria: no SQLite o BEGIN é deferido e as duas leem antes de qualquer uma escrever. serialize.ts enfileira por lista/board — com o limite documentado de valer para um processo só.

Antes: 5/15 distintas. Depois: 20/20 e 10/10.

TEXT vs INTEGER no SQLite (caefe45). O backfill do Prisma grava DEFAULT CURRENT_TIMESTAMP como TEXT, mas o Prisma lê DateTime como INTEGER, e o SQLite ordena por classe de tipo antes do valor. newestCard retornava um card do seed com a data correta no JSON — que é justamente o que esconde o bug.

Mais dois meus, corrigidos aqui: contagem do esvaziar lixeira ignorava cards vivos levados pelo cascade, e um hook depois de return condicional que deixava a tela em branco.

Vinte melhorias

Backend — Lixeira com soft delete (6 rotas), log de atividade com 12 pontos de instrumentação, GET /api/stats, testes unitários sem dependência nova, ws-stress.mjs (10 clientes WebSocket em rajada).

Frontend — janela da Lixeira, ícones em pixel art, Campo Minado jogável, Bloco de Notas, Propriedades do Sistema, Visualizador de Eventos, sons sintetizados na Web Audio API, cinco esquemas de cores, busca com realce, atalhos, desfazer, menu de contexto, relógio, protetor de tela.

Infra — README, LICENSE, CONTRIBUTING, templates, CI com quatro camadas, e check-hooks.mjs, que pega no build o bug que nem tsc nem Vite detectam.

Verificação

Local CI
Testes unitários 17/17 pass 17
Smoke (Docker) 82/82 SMOKE OK
Concorrência WS 18/18 WS-STRESS OK
tsc --noEmit limpo success
Guarda de hooks 23 arquivos success

Duas decisões que merecem revisão

Acessibilidade (docs/ACESSIBILIDADE.md): 22 achados. O bloqueante é que o card é um <article draggable> sem tabIndex nem onKeyDown — a função principal do app é inacessível por teclado. O contraste de .card-dimmed foi medido em 2,42:1 contra os 4,5:1 da WCAG AA. Documentei sem corrigir: são mudanças de comportamento que merecem PR próprio.

Ícones: recriações próprias em SVG, não os arquivos da Microsoft — os originais são proprietários e este repositório é público.

🤖 Generated with Claude Code

renanlocatizfernandes and others added 19 commits August 21, 2026 01:37
DELETE de card e de lista passa a marcar `deletedAt` em vez de apagar. O item
some do board, aparece em GET /api/trash e pode voltar. So as rotas /api/trash
apagam de verdade.

O contrato antigo continua valendo: DELETE responde 204 e emite card.deleted /
list.deleted com { id }, entao o reducer do cliente nao muda. Restaurar reusa
card.created / list.created em vez de eventos novos -- o upsert idempotente por
id ja faz a coisa certa com eles.

A armadilha do soft delete e que findUnique por id continua achando a linha:
toda rota de recurso vivo passa a filtrar deletedAt: null via findLiveCard() /
findLiveList(), senao da para renomear, mover e criar cards dentro de coisas
que o board nao mostra mais.

Duas semanticas deliberadas: apagar uma lista nao marca os cards dela (senao um
card jogado fora sozinho ressuscitaria junto com ela), e restaurar um card cuja
lista esta na lixeira retorna 409, sinalizado antes pelo originalList.deleted.

Smoke: 45 -> 82 assercoes, incluindo preflight de CORS das rotas novas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Lixeira ganha UI: janela com Restaurar/Excluir por item, Esvaziar no rodape, e
o botao Restaurar ja nasce desabilitado quando originalList.deleted avisa que o
restore vai dar 409 -- evita o erro em vez de exibi-lo.

Junto, os tres achados de QA que estavam abertos no useBoard.ts:

- race no load(): um GET /api/board em voo podia sobrescrever eventos WS mais
  novos. Resolvido com numero de geracao que descarta resposta obsoleta.
- `incoming.cards ?? l.cards` nunca usava o fallback ([] nao e nullish), entao
  um list.updated com cards vazio zerava a lista. Agora testa a presenca da
  chave, o que tambem e o que faz o restore de lista trazer os cards de volta.
- evento sobre card ou lista desconhecida sumia em silencio ate o proximo
  reload. Agora marca o board como stale e o efeito recarrega.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Emoji entregava na hora que nao e Windows 95. Os icones agora sao SVG desenhados
numa grade de 16x16 com a paleta VGA de 16 cores: lixeira vazia e cheia, bandeira
do Iniciar, pasta, documento, aviso, bomba e bandeira do Campo Minado.

Sao recriacoes proprias, nao os arquivos da Microsoft -- os originais sao
proprietarios e este repositorio e publico. O que da a aparencia certa e a
disciplina do formato, nao o arquivo.

shapeRendering="crispEdges" e obrigatorio: sem ele o navegador aplica antialias
e o resultado parece um icone moderno borrado. Escalar so em multiplos inteiros
do tamanho base -- 16px renderizado em 20px mostra linhas de espessura irregular,
que e o detalhe que denuncia a imitacao mais rapido que qualquer outro.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O seed sempre teve um card "Jogar Campo Minado". Agora ele abre o jogo: 9x9 com
10 minas, flood fill, bandeira com botao direito, cronometro e a carinha que
reinicia. Qualquer card com esse titulo funciona -- nao ha id magico.

As minas so sao sorteadas depois do primeiro clique, excluindo a celula clicada e
a vizinhanca dela: garante que a primeira jogada sempre abre uma area em vez de
um quadradinho, e torna impossivel perder de cara.

O protetor de tela aparece apos 2 minutos. So input humano conta como atividade:
se trafego de WebSocket resetasse o timer, ele nunca apareceria com duas abas
abertas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Busca por titulo e descricao com N/T/R/? e / como atalhos, janela de ajuda e
relogio na bandeja.

A busca realca os cards que casam e esmaece o resto, em vez de filtrar. Esconder
os que nao casam mudaria os vizinhos de cada card, e como positionFor() calcula a
posicao a partir dos vizinhos do destino, arrastar durante uma busca gravaria a
posicao errada.

Os atalhos ficam desligados enquanto ha janela aberta ou o foco esta num campo:
senao "n" digitado num dialogo criaria uma lista no board por tras dele.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O unico teste era o smoke, que e integracao e exige o stack de pe. Agora ha
`npm test` com o runner nativo do Node (node:test), sem dependencia adicional:

- position.test.mjs: a matematica da position fracionaria, incluindo o limite de
  ~53 divisoes pela metade antes do float empatar.
- trash-semantics.test.mjs: as duas regras da Lixeira que mais surpreendem, como
  funcoes puras estado -> estado.

Junto: .env.example com os defaults reais lidos do server.ts, e reset-db.mjs, que
e dry-run por padrao e so destroi com --yes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README, LICENSE (MIT), CONTRIBUTING, templates de issue e PR, e CI no GitHub
Actions com tres jobs: typecheck do backend, build do frontend, e o smoke test
contra o stack Docker de verdade.

O .editorconfig e o .gitattributes forcam LF: este repo ja teve um arquivo
gravado em UTF-16 CRLF por redirecionamento do PowerShell, que o bash nao
consegue interpretar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md e docs/API.md documentam a Lixeira por inteiro, as regras dos icones em
pixel art e por que a busca realca em vez de filtrar. FRONTEND-REQUESTS.md
registra o handoff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…anco

O useMemo da busca tinha ficado abaixo do `if (!board) return <Frame>`. Isso muda
a quantidade de hooks entre a primeira renderizacao e a seguinte, e o React
derruba a arvore inteira -- a pagina servia o bundle certo e nao desenhava nada.

Movido para junto dos outros hooks, com `!board` tratado dentro do memo. Todos os
13 hooks do App agora ficam acima de qualquer return condicional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O workflow foi escrito antes de `npm test` existir, entao os 17 testes unitarios
nao rodavam no CI -- so o smoke, que precisa do stack de pe. Agora o job de
backend faz typecheck e testes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Arrastar um card passa a oferecer "Desfazer" por 8 segundos, tambem em Ctrl+Z.
A origem e capturada ANTES do move: depois dele o eco do WebSocket ja reescreveu
o card e nao ha mais de onde tirar o lugar antigo.

E so uma acao, com validade curta, e nao ha refazer. O estado aqui e compartilhado
entre abas: desfazer tres passos atras pode reverter algo que outra pessoa mudou
no meio tempo, e refazer depois disso e pedir conflito.

Botao direito no fundo do desktop abre menu de contexto. Ele se reposiciona
sozinho perto das bordas -- abre para a esquerda em vez de vazar da viewport --
e isso exige medir depois de montar, dai o useLayoutEffect: com useEffect o menu
pisca na posicao errada antes de corrigir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O bug que deixou a tela em branco nao e pego por nada: tsc compila, o Vite
builda, o bundle e servido, e o React derruba a arvore em silencio porque a
quantidade de hooks mudou entre renders.

`check-hooks.mjs` varre os componentes e falha se achar chamada de hook depois
de um return de nivel de componente. Roda no `npm run build`, entao o CI pega
antes do deploy. Verificado dos dois lados: injetando a violacao ele acusa
arquivo e linha e sai 1; sem ela, passa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tres janelas classicas do shell, mais o audio.

Os sons sao sintetizados na Web Audio API, nao arquivos .wav: o container builda
offline e binario no repo e peso morto. Comecam DESLIGADOS e ficam no
localStorage -- audio automatico sem o usuario pedir e hostil. O AudioContext e
criado preguicosamente no primeiro play() porque navegador bloqueia audio antes
da primeira interacao, e nenhuma excecao de audio escapa: som e enfeite, falha
dele nao pode derrubar a UI.

O `runWithSound` envolve o `run` que ja centraliza toda mutacao, entao nenhuma
chamada individual precisa lembrar de tocar, e o som de erro acompanha qualquer
falha de API automaticamente.

O Bloco de Notas salva no localStorage, nao no servidor: nao existe recurso de
"nota" no contrato da API e inventar um por conta propria seria mudanca de
contrato unilateral -- exatamente o que docs/BACKEND-REQUESTS.md existe para
evitar.

Junto vao useWindowManager e DraggableWindow (arrastar pela title bar com pointer
events, nao HTML5 drag, que ja e usado pelos cards e os dois brigariam), ainda
nao ligados na UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`POST /api/cards` e `POST /api/lists` liam a maior position e so depois
inseriam `last.position + 1`. Duas criacoes simultaneas leem a mesma "ultima" e
nascem empatadas: 15 requisicoes em paralelo produziam 5 positions distintas.

O empate nao e cosmetico. A partir de dois cards na mesma position,
positionFor() calcula (a + b) / 2 === a para o slot entre eles -- o servidor
responde 200, grava o que recebeu, e o card NAO SAI DO LUGAR. O desempate por id
no orderBy mantem a ordem estavel, entao nada parece quebrado; o drag
simplesmente para de funcionar.

Transacao do Prisma nao resolveria sozinha: no SQLite o BEGIN e deferido, o
SELECT nao pega lock de escrita, e as duas leem antes de qualquer uma escrever.
`serialize.ts` enfileira por lista/board. E um mutex em memoria, valido para um
processo -- que e o modelo aqui; com mais instancias a correcao passa a ser
INSERT ... SELECT MAX(position)+1 numa declaracao so.

Achado pelo ws-stress.mjs, que sobe 10 clientes WebSocket e dispara mutacoes em
rajada. Ele tambem prova o que o CLAUDE.md exige e ninguem testava: os 10
clientes recebem eventos identicos, o replay do log e idempotente, e o estado
derivado so dos eventos bate com um GET /api/board fresco.

Antes: 5/15 distintas. Depois: 20/20 e 10/10. Agora roda no CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Log append-only de toda mutacao, com a frase ja formatada -- o cliente imprime
`summary`, nunca remonta o texto a partir de ids. Doze pontos de instrumentacao
em cards/lists/trash, cada um logo depois do broadcast correspondente, para o
log so registrar o que ja foi persistido e anunciado.

Gravar log nunca derruba a mutacao principal: `record()` engole a propria falha.
Um log de auditoria que quebra a escrita e pior do que nao ter log.

A janela escuta `activity.recorded` num socket proprio em vez de passar pelo
reducer do board: o log nao e estado do board, e enfia-lo la poluiria o reducer
que hoje so lida com listas e cards.

Junto vai `GET /api/stats` (contagens, cards por lista, mais antigo/mais novo),
que exigiu `createdAt` na model Card.

Bug corrigido na migration: o backfill do Prisma grava DEFAULT CURRENT_TIMESTAMP
como TEXT, mas o Prisma le DateTime como INTEGER. O SQLite ordena por classe de
tipo antes do valor, entao toda linha antiga ficava depois de qualquer linha
nova -- `newestCard` devolvia um card do seed COM A DATA CERTA no JSON, que e o
que esconde o bug. A segunda migration normaliza via julianday.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Os cinco esquemas de Painel de Controle > Video: Windows padrao, Chocolate,
Ameixa, Deserto e Meia-noite. Nomes e cores sao os originais -- sao fatos sobre
um produto historico, nao arte copiada; o que nao da para reproduzir e o bitmap
dos papeis de parede, entao o fundo e gradiente CSS.

Trocar tema significa sobrescrever o que o 98.css assume fixo. Aplicado com
moderacao: so o fundo e a barra de titulo, que e o que muda de verdade entre os
esquemas. A escolha fica no localStorage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vinte e dois achados com arquivo:linha, severidade e correcao proposta.

O bloqueante: o card e um <article draggable> sem tabIndex, sem onKeyDown e sem
role, aberto so por duplo-clique. Ele nao recebe foco, nao abre e nao se move
pelo teclado -- tabular por quatro listas toca doze controles e zero cards. A
funcao principal do app e inacessivel por teclado hoje.

O contraste de `.card-dimmed` foi medido, nao estimado: opacity 0.35 compoe
2,42:1, contra os 4,5:1 que a WCAG AA exige.

Documento e diagnostico, nao correcao -- as mudancas ficam para PRs proprios,
onde cada uma pode ser revista isoladamente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ficaram de fora do commit anterior. Restaurar e apagar definitivamente sao
justamente as acoes que mais interessam num log de auditoria -- sao as que o
usuario nao consegue desfazer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documenta as rotas novas e, principalmente, a invariante que o race expos: toda
rota que calcula position a partir do estado atual precisa envolver leitura e
escrita em serialize(). O sintoma quando se esquece disso e o drag parar de
funcionar sem nenhum erro aparente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@renanlocatizfernandes renanlocatizfernandes changed the title Lixeira do Windows 95, Campo Minado e padronização do repositório Lixeira, aplicativos do Win95 e correção de dois bugs de concorrência Aug 21, 2026
@renanlocatizfernandes
renanlocatizfernandes merged commit c1ff22f into master Aug 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant