Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b11c4c8
feat(backend): Lixeira do Windows 95 com soft delete e restauracao
renanlocatizfernandes Aug 21, 2026
1177219
feat(frontend): janela da Lixeira e correcoes do reducer
renanlocatizfernandes Aug 21, 2026
62361da
feat(frontend): icones do Windows 95 em pixel art
renanlocatizfernandes Aug 21, 2026
75f7641
feat(frontend): Campo Minado jogavel e protetor de tela
renanlocatizfernandes Aug 21, 2026
6b1dd99
feat(frontend): busca, atalhos de teclado e relogio na bandeja
renanlocatizfernandes Aug 21, 2026
9af69e2
test(backend): testes unitarios sem dependencia nova
renanlocatizfernandes Aug 21, 2026
66db4fa
chore: padronizacao do repositorio
renanlocatizfernandes Aug 21, 2026
91a4c00
docs: contrato da Lixeira, icones e busca
renanlocatizfernandes Aug 21, 2026
f9d2402
fix(frontend): hook depois de return condicional deixava a tela em br…
renanlocatizfernandes Aug 21, 2026
4d3c258
ci: roda os testes unitarios no job do backend
renanlocatizfernandes Aug 21, 2026
8e5256a
feat(frontend): desfazer de movimento e menu de contexto
renanlocatizfernandes Aug 21, 2026
86d6d67
build(frontend): guarda contra hook depois de return condicional
renanlocatizfernandes Aug 21, 2026
6891b26
feat(frontend): Bloco de Notas, Propriedades do Sistema e sons
renanlocatizfernandes Aug 21, 2026
3c11221
fix(backend): race de position no POST tornava o drag inoperante
renanlocatizfernandes Aug 21, 2026
caefe45
feat: Visualizador de Eventos e endpoint de estatisticas
renanlocatizfernandes Aug 21, 2026
2914496
feat(frontend): esquemas de cores do Windows 95
renanlocatizfernandes Aug 21, 2026
c0d9dcf
docs: auditoria de acessibilidade
renanlocatizfernandes Aug 21, 2026
1ea9bed
feat(backend): instrumenta as rotas da lixeira no log de atividade
renanlocatizfernandes Aug 21, 2026
c1ff22f
docs: activity, stats e a regra de concorrencia da position
renanlocatizfernandes Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# EditorConfig -- https://editorconfig.org
# LF em tudo, sempre. Este repo ja teve um arquivo salvo em UTF-16 com CRLF que
# quebrou o build de forma nao obvia; charset e end_of_line aqui nao sao estilo,
# sao protecao.
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
# Markdown usa dois espacos no fim da linha como quebra de linha.
trim_trailing_whitespace = false

[*.{yml,yaml,json,jsonc}]
indent_size = 2

[Makefile]
indent_style = tab
35 changes: 35 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Normaliza tudo para LF no repositorio, independente do SO de quem commita.
* text=auto eol=lf

# Fontes e configuracao: texto, sempre LF.
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.mjs text eol=lf
*.json text eol=lf
*.css text eol=lf
*.html text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.prisma text eol=lf
*.sql text eol=lf
*.sh text eol=lf
.gitignore text eol=lf
.dockerignore text eol=lf
.editorconfig text eol=lf
Dockerfile text eol=lf

# Binarios: o git nao mexe no conteudo nem tenta fazer diff.
*.png binary
*.ico binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.woff binary
*.woff2 binary
*.db binary

# Lockfiles nao poluem o diff da revisao.
package-lock.json linguist-generated=true -diff
32 changes: 32 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: Bug
about: Algo não funciona como deveria
title: "fix: "
labels: bug
---

## O que acontece

<!-- Uma frase. -->

## O que deveria acontecer

## Como reproduzir

1.
2.
3.

## Ambiente

- Modo: <!-- docker compose | local (npm run dev) -->
- Porta aberta no browser: <!-- 8090 | 5173 -->
- Browser e SO:
- Abas abertas simultaneamente: <!-- importante em bugs de sincronização/WebSocket -->

## Erros no console / logs

<!-- Console do browser, aba Network, e `docker compose logs backend`. -->

```
```
23 changes: 23 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: Feature
about: Sugerir uma funcionalidade ou melhoria
title: "feat: "
labels: enhancement
---

## O problema

<!-- O que hoje é difícil ou impossível. Não descreva a solução aqui. -->

## Proposta

<!-- O que você quer que aconteça, do ponto de vista de quem usa o board. -->

## Impacto no contrato

- [ ] Nenhum — só frontend ou só backend, sem mudar a API
- [ ] Muda a API — precisa de atualização em `CLAUDE.md` e `docs/API.md`

<!-- Se muda: quais endpoints, payloads ou eventos de WebSocket. -->

## Alternativas consideradas
28 changes: 28 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## O que muda

<!-- Uma ou duas frases. Se fecha uma issue: "Fecha #123". -->

## Por quê

<!-- O problema que isto resolve. -->

## Como testar

<!-- Passos exatos. Ex.: docker compose up --build, abrir :8090 em duas abas,
arrastar um card da primeira lista para a segunda. -->

## O contrato mudou?

- [ ] Não
- [ ] Sim — e atualizei `CLAUDE.md` **e** `docs/API.md` neste mesmo PR

<!-- Conta como contrato: payload, status code, evento de WebSocket, semântica
de `position`, portas/origens de CORS, ou quem é dono de qual diretório. -->

## Checklist

- [ ] Título em Conventional Commits (`feat(frontend): ...`)
- [ ] `npx tsc --noEmit` no backend e `npm run build` no frontend passam
- [ ] `node backend/scripts/smoke.mjs` passa contra o stack de pé
- [ ] Não editei o diretório da outra sessão (`backend/` vs `frontend/`)
- [ ] Sem `node_modules/`, `dist/`, `.env` ou `*.db` no diff
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
backend:
name: backend (typecheck)
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: backend/package-lock.json

- name: Install
run: npm ci

# Sem prisma generate o Prisma Client nao existe e o tsc quebra em
# arquivos que nao tem relacao com o schema.
- name: Prisma generate
run: npx prisma generate

- name: Typecheck
run: npx tsc --noEmit

# Testes unitarios: runner nativo do Node, sem dependencia extra. Rodam
# sem banco e sem stack de pe -- o smoke, que exige os dois, e outro job.
- name: Testes unitarios
run: npm test

frontend:
name: frontend (build)
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install
run: npm ci

# "npm run build" e "tsc -b && vite build": typecheck e bundle juntos.
- name: Build
run: npm run build

smoke:
name: smoke (stack completo)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: backend/package-lock.json

# smoke.mjs importa "ws"; ele vem das dependencias do backend.
- name: Install backend deps
run: npm ci
working-directory: backend

- name: Sobe o stack
run: docker compose up -d --build

- name: Espera o healthcheck do backend
run: |
for i in $(seq 1 60); do
status=$(docker compose ps --format '{{.Health}}' backend | head -n 1)
echo "tentativa $i: health=${status:-<vazio>}"
if [ "$status" = "healthy" ]; then
echo "backend saudavel"
exit 0
fi
if [ "$status" = "unhealthy" ]; then
echo "backend unhealthy"
exit 1
fi
sleep 5
done
echo "timeout esperando o healthcheck do backend"
exit 1

- name: Smoke test do contrato
run: node backend/scripts/smoke.mjs http://localhost:3001

# Concorrencia: 10 clientes WS, mutacoes em rajada. Pegou um race real de
# read-modify-write no POST que o smoke sequencial nunca acionaria.
- name: Teste de concorrencia do WebSocket
run: node backend/scripts/ws-stress.mjs http://localhost:3001

- name: Frontend responde na 8090
run: curl -fsS -o /dev/null http://localhost:8090

- name: Logs do backend (em caso de falha)
if: failure()
run: docker compose logs backend

- name: Derruba o stack
if: always()
run: docker compose down -v
67 changes: 64 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,37 @@ A consequência é que o cliente precisa aplicar todo evento como um **upsert id

Formato: `{ "type": "card.moved", "payload": { ... } }`. O payload de movimento é só `{ id, listId, position }`, nunca o board inteiro.

Eventos: `list.created`, `list.updated`, `list.deleted`, `card.created`, `card.updated`, `card.moved`, `card.deleted`.
Eventos: `list.created`, `list.updated`, `list.deleted`, `card.created`, `card.updated`, `card.moved`, `card.deleted`, `trash.updated`, `activity.recorded`.

### A Lixeira: DELETE é soft delete

`DELETE /api/cards/:id` e `DELETE /api/lists/:id` **não apagam** — marcam `deletedAt` e o
item vai para a lixeira, de onde pode voltar. Só as rotas `/api/trash` apagam de verdade.

A consequência que pega desprevenido: `findUnique({ where: { id } })` continua achando a
linha. **Toda rota que opera sobre um recurso vivo precisa filtrar `deletedAt: null`** —
use `findLiveCard()`/`findLiveList()` de `backend/src/trash.ts` em vez de escrever o where
na mão. Sem isso dá para renomear, mover e criar cards dentro de coisas que o board não
mostra mais.

Mandar uma lista para a lixeira **não** marca os cards dela: eles somem porque a lista
sumiu, e voltam junto na restauração. Marcar em cascata ressuscitaria cards que já tinham
sido jogados fora sozinhos.

Restaurar reusa `card.created`/`list.created` em vez de inventar `*.restored`: o cliente
já aplica esses eventos como upsert por `id`, então o item reaparece sem mudança no
reducer. `trash.updated` carrega **só a contagem** `{ cards, lists }` — o conteúdo vem de
`GET /api/trash` quando a janela abre.

### Concorrencia: ler-e-inserir precisa ser atomico

Toda rota que calcula uma position a partir do estado atual (`POST /api/cards`,
`POST /api/lists` — ambas fazem "ultima + 1") tem que envolver a leitura **e** a
escrita em `serialize()` de `backend/src/serialize.ts`. Sem isso duas criacoes
simultaneas leem a mesma "ultima" e nascem empatadas, e dois itens empatados
tornam o drag entre eles inoperante: `positionFor()` devolve `(a+b)/2 === a`, o
servidor responde 200 e o card nao se move. O desempate por `id` esconde o
sintoma. Detalhes no fim de `docs/API.md`.

### API REST

Expand All @@ -87,11 +117,22 @@ GET /api/health
GET /api/board board default com lists e cards aninhados, já ordenados
POST /api/lists { title }
PATCH /api/lists/:id { title?, position? }
DELETE /api/lists/:id cascateia nos cards
DELETE /api/lists/:id -> lixeira, leva os cards junto
POST /api/cards { listId, title, description? }
PATCH /api/cards/:id { title?, description? }
PATCH /api/cards/:id/move { listId, position } <- o drag-and-drop
DELETE /api/cards/:id
DELETE /api/cards/:id -> lixeira, nao apaga

GET /api/trash { cards, lists, counts }
POST /api/trash/cards/:id/restore 409 se a lista de origem esta na lixeira
POST /api/trash/lists/:id/restore
DELETE /api/trash/cards/:id apaga de vez
DELETE /api/trash/lists/:id apaga de vez
DELETE /api/trash esvaziar lixeira

GET /api/activity?limit=50 log de atividade, mais recente primeiro
DELETE /api/activity limpa o log
GET /api/stats contagens e agregados do board
```

`GET /api/board` é a única leitura de que o frontend precisa; depois dela o estado se mantém pelo WebSocket. Não crie endpoints `GET` por recurso individual sem necessidade real.
Expand All @@ -101,3 +142,23 @@ DELETE /api/cards/:id
O frontend usa o pacote npm **`98.css`** (não CDN — o container precisa buildar offline). Cada lista é uma `.window` com `.title-bar`, cada card é um item dentro de `.window-body`. Não escreva CSS que recrie bordas ou botões que o `98.css` já fornece; use as classes dele e limite o CSS próprio a layout (o board é um flex row com scroll horizontal).

Drag-and-drop é HTML5 nativo (`draggable`, `dragover`, `drop`) — sem biblioteca.

### Ícones

`frontend/src/icons.tsx` tem os ícones em SVG pixel art, desenhados numa grade de
16x16 com a paleta VGA de 16 cores. **São recriações próprias, não os arquivos da
Microsoft** — os ícones originais do Windows 95 são proprietários e este repositório
é público.

Três regras mantêm a aparência: `shapeRendering="crispEdges"` (sem isso o navegador
aplica antialias e o resultado parece um ícone moderno borrado), escalar só em
múltiplos inteiros do tamanho base (16, 32, 48 — um ícone de 16px renderizado em
20px mostra linhas de espessura irregular), e nada de emoji na UI: é o detalhe que
denuncia a imitação mais rápido que qualquer outro.

### Busca: realce, não filtro

A busca marca os cards que casam e esmaece o resto, em vez de esconder os que não
casam. Filtrar mudaria os vizinhos de cada card, e como `positionFor()` calcula a
posição nova a partir dos vizinhos do destino, arrastar durante uma busca gravaria
a posição errada.
Loading
Loading