Repository navigation
Lixeira, aplicativos do Win95 e correção de dois bugs de concorrência - #1
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dezenove commits. Seis agentes em paralelo. Tudo verificado contra o stack Docker e reverificado no CI.
Dois bugs reais, achados por agentes
Race de
positionnoPOST(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 === apara o slot entre eles — o servidor responde 200 OK e o card não sai do lugar. O desempate poridnoorderBymantinha 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.tsenfileira 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 gravaDEFAULT CURRENT_TIMESTAMPcomo TEXT, mas o Prisma lêDateTimecomo INTEGER, e o SQLite ordena por classe de tipo antes do valor.newestCardretornava 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 nemtscnem Vite detectam.Verificação
pass 17SMOKE OKWS-STRESS OKtsc --noEmitDuas decisões que merecem revisão
Acessibilidade (
docs/ACESSIBILIDADE.md): 22 achados. O bloqueante é que o card é um<article draggable>semtabIndexnemonKeyDown— a função principal do app é inacessível por teclado. O contraste de.card-dimmedfoi 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