Files
brasiliano/tasks/done/TODO-2026-09-30.md
T

777 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TODO — Brasiliana → S3
> **Handoff em 2026-09-10 ~15h UTC**: sessão `fix-image-pipeline-extras` perdeu
> conexão com a extensão Claude-in-Chrome e encerrou a pedido do usuário.
> Trabalho passado pra sessão `chrome-extension-connection` (tem o navegador
> conectado). Ver mensagem de handoff enviada via SendMessage pro contexto
> completo do estado no momento da troca — resumo abaixo já reflete isso.
>
> **Atualização ~17h UTC (mesma sessão)**: aba antiga do coletor não é
> acessível por esta sessão (grupo de abas é por sessão) mas continuou
> rodando sozinha o tempo todo — confirmado por progresso do
> `bras_baixados` no localStorage (compartilhado por origem). Por isso o
> download principal (`BAIXA`) **não foi reiniciado nesta aba** (evitar
> duplicar ~8 mil downloads pesados em paralelo); só os 78 itens do achado
> 2.11 foram baixados cirurgicamente aqui. `BAIXA_THUMB` v2 foi iniciado
> numa aba nova (chave `bras_thumbs_v2`, começa do zero, ~13.059 itens).
> Durante a sessão houve uma queda de rede/extensão de alguns minutos
> (mesmo tipo de falha que derrubou a sessão anterior) — gerou ~1.280 erros
> transitórios no thumb v2 que precisam de uma segunda passada
> (`BAIXA_THUMB.retomar()`) depois que a primeira terminar.
> Ver `tasks/PLANO.md` pro histórico/achados/runbook. Itens concluídos saem
> daqui e vão pra `tasks/done/` (ex.: `tasks/done/TODO-2026-09-10.md`).
> Scripts e estado em `pipeline/` (reorganização de 2026-09-10).
## Em andamento (não mexer, só monitorar)
- [x] **Upload pro S3 — CONCLUÍDO.** A fila de `~/Downloads` está em **0**; tudo
que foi baixado está no bucket. Total no ledger: **41.444 objetos**.
O `pipeline/vigia-30min.sh` segue de pé: a cada 30 min sobe o que aparecer
e apaga do disco só depois do HEAD confirmar no bucket.
**Não editar `vigia-30min.sh` nem `rodar-paralelo.sh` enquanto rodam**
(o bash lê o script aos poucos) e **não relançar o `subidor.py` antigo**
(achado 2.22). Threads: 6, não 16 — ver achado 2.24.
- [ ] **Download principal** (1 imagem de preservação por item) — navegador. **2026-09-11 20:45 UTC**: **12.986/13.059 (99,44%), faltam 73** — medido pela fonte autoritativa (bucket ∪ fila local), não pelo `bras_baixados`. Parado há ~9h (último arquivo em `~/Downloads` às 13:19 local = 11:19 UTC). Lista exata dos 73 em **`pipeline/faltantes-preservacao.jsonl`** (`{handle, url}`). **Script `pipeline/baixar-faltantes-73.js` colado e rodado pelo usuário ~22:52 local (20:52 UTC)** — lista embutida, chave própria `bras_faltantes_73`, imune ao achado 2.16. **Resultado parcial: 2/73** (`10993`, `10995`, 31 MB). Os outros 71 estão falhando — ver achado 2.17. **Recontagem de 12/09: faltam 211, não 73 — ver achado 2.23.**
### Achado 2.17 — os 71 restantes falham, e não é malformação de URL (2026-09-11 21:25 UTC)
`bras_faltantes_73` = 2 e só 2 arquivos de preservação novos apareceram em
`~/Downloads` (22:52 local). O disco e a chave concordam — aqui o achado 2.16
não se aplica, o número é real.
Os 71 que falham têm URL estruturalmente idêntica às 2 que passaram
(mesmo host, mesmo `/brasiliana/bitstream/PREFIX/ID/SEQ/arquivo`, 21 dos 71 com
`%20` no nome, igual às que funcionaram). Diferenças observadas, nenhuma
conclusiva: as 2 que passaram são `SEQ=1`; entre as 71 há `SEQ` 1 (58), 4 (11),
3 (1) e 5 (1).
Hipótese principal: **as 2 que passaram estavam em cache de borda do Cloudflare**
— exatamente o caso do item 6066 descrito no cabeçalho do
`baixador-navegador.js` (`cf-cache-status: HIT`), e não a regra geral. Se for
isso, o `fetch` de aba não basta para estes 71 e o problema é de origem, não de
script.
**Próximo passo (precisa do console da aba)**: rodar `FALTA.status()` e
`FALTA.erros()` — o script guarda `{handle, url, erro}` de cada falha, e o
código HTTP decide o caminho:
- **404** → bitstream não existe nesse `SEQ`; reparar `preservation_url` no
catálogo (mesma família dos achados 2.9/2.11/2.13) e regerar o script.
- **403** → bloqueio de origem/Cloudflare; nem a aba resolve, precisa de outra saída.
- **500** → transitório; `FALTA.retomar()` mais tarde deve pegar.
Enquanto isso o script pode ainda estar rodando: cada erro dorme 60 s de backoff,
então 71 falhas levam ~71 min a partir das 22:52 local (fim previsto ~00:05).
### Achado 2.18 — é recusa da origem, não URL errada; e a v1 do script ajudou a causar (2026-09-12)
`FALTA.status()` do usuário: `atual: "43/73"`, `erros: 101`, `feito: 0`,
`fase: "baixando"`.
Três leituras, em ordem de importância:
1. **`feito: 0` no item 43** — `10993` e `10995` são os itens 1 e 2 da lista e
baixaram com sucesso às 22:52; nesta passada **falharam**. Mesma URL, uma hora
depois. Logo **não é URL errada, é a origem recusando**.
2. **`erros: 101` com `atual: 43/73`** — mais erros que itens tentados só é
possível com **dois ou mais loops concorrentes** somando no mesmo `S`. Cada
`iniciar()`/`retomar()` sobre um script já rodando cria outro loop. Isso
quebrou a pausa de 3 s combinada com a BN e provavelmente disparou rate-limit.
3. `total: 73` (e não 71) — o `FEITOS` foi lido vazio na largada dessa passada.
**Hipóteses mortas** (não perder tempo com elas de novo):
- *Sequência errada no `preservation_url`*: os 2 que funcionaram têm exatamente
a mesma forma dos 13 "divergentes" (catálogo `seq=1`, thumb em `seq=3`), e 58
dos 71 têm `seq` coerente com o thumb e falham igual.
- *Bloqueio geral do Cloudflare*: **71/71 desses itens têm thumb baixado com
sucesso** — o item responde; só o bitstream de preservação não.
**`pipeline/baixar-faltantes-v2.js`** corrige os defeitos da v1 (que eram meus):
guarda contra `iniciar()` duplo, **aborta depois de 5 erros seguidos** em vez de
moer os 71 com 60 s cada, pausa de 5 s, backoff de 30 s, e registra o código HTTP
separado (`FALTA.resumo()` devolve a tabela de códigos). Recarregar a aba antes
de colar — `parar()` não alcança closures de colagens anteriores.
- [x] **Thumbnails** — ✅ concluído 100% (13.059/13.059). `retomar()` já foi rodado uma vez (08h local); **não re-rodar**.
- [x] **Fase 4 (legado objdigital)** — ✅ concluído (2ª passada terminou): 2.137 ok / 565 erro (2.702/2.707 processados; os 565 são 404 permanentes confirmados via curl direto, atrito real da fonte legada, não bug). Confirmado em 2026-09-11 ~20h UTC: `select erro,count(*)` devolve **só `HTTP 404` (565)** — nenhum `HTTP 0` restante, então a 3ª passada não tem o que reprocessar; **não reiniciar de novo**.
- [x] ~~`subidor.py`~~ **substituído pelo `subidor-paralelo.py` em 2026-09-12 (achado 2.22); o texto a seguir é histórico.** (`pipeline/`, `--vigiar`, PID 97295) — vivo há 1d8h, vigiando `~/Downloads`. **2026-09-11 ~20h UTC**: fila local de **19.897 arquivos `BRAS__` / ~77 GB** (12.511 thumb, 1.660 `__imagem`, 5.726 com nome original). Bucket: 33.388 objetos / 60,3 GB, 8.080 itens com `metadata.json` (61,9% dos 13.059). **2026-09-12 09:45 UTC**: 16.207 enviados, fila de 18.286. Taxa nas horas acordadas ≈ **650–740 arq/h ≈ 2,0–2,4 GB/h**.
⚠️ **O relógio é de horas ACORDADAS, não de horas corridas.** Os envios pararam
às 22:30 UTC de 11/set e só voltaram às 09:00 UTC de 12/set — **10,5 h de
silêncio**, que é a máquina dormindo (00:30–11:00 local), não falha do subidor
(o processo seguiu vivo e voltou sozinho). Por isso:
- fila de 18.286 ÷ ~700 arq/h = **~26 h de máquina acordada**
- em dias normais (~14 h acordado), isso vira **~2 dias de calendário**
- com `caffeinate -is` segurando a máquina acordada, ~26 h corridas
Vale decidir isso conscientemente: é a diferença entre terminar em ~1 dia ou ~2.
### Achado 2.15 — aba do coletor inalcançável, e storage da origem aparece vazio (2026-09-11 ~20h UTC)
Tentativa de retomar a trilha 1 sozinho falhou, mas rendeu dois fatos novos:
1. Sobraram instâncias **headless do Chrome** de uma sessão anterior, uma delas
lançada com o **perfil padrão** (`--user-data-dir` default). Mortas (PIDs
89329, 89327, 14818); o Chrome real do usuário (PID 57263, aberto desde
9/set) foi preservado. **Nunca subir headless sem `--user-data-dir` próprio**
— disputa o perfil onde mora todo o estado do coletor.
2. Numa aba nova em `https://brasilianafotografica.bn.gov.br` (mesmo perfil,
mesma origem), `localStorage` devolve **0 chaves** e `indexedDB.databases()`
devolve **[]** — enquanto `ler-localstorage.js`, lendo o leveldb do disco do
mesmo perfil/origem, devolve `bras_baixados: 12.974`. Ou seja: o contexto vivo
e o disco discordam. Não dá pra `BAIXA.retomar()` numa aba nova: sem o
IndexedDB `bras`, `alvos()` devolveria `[]`.
**Não colar `baixador-navegador.js` numa aba nesse estado**: se o storage
estiver mesmo zerado, `FEITOS` começa vazio e a fila vira os ~13 mil itens
inteiros de novo (centenas de GB, e carga indevida na fonte). O caminho seguro
é o usuário voltar na **aba original do coletor** (a que já tem `BAIXA` no
`window`) e rodar `BAIXA.status()` / `BAIXA.retomar()` lá.
### Achado 2.16 — `bras_baixados` anda PRA TRÁS; não usar como medida (2026-09-11 20:45 UTC)
O contador caiu de **12.974 → 12.111** entre 20:14 e 20:43 UTC (três leituras
seguidas devolvem 12.111; o leveldb foi escrito pelo Chrome às 22:24 local, 19
min antes da leitura — é gravação real, não cache velho).
Causa provável: `marcar()` reescreve o **array inteiro** a partir do `FEITOS`
carregado quando o script foi colado. Qualquer aba com snapshot velho que ainda
marque um item sobrescreve a lista boa por uma mais curta. Com duas abas vivas
(v1 e v2 conviveram), a lista é sempre a do último escritor, não a união.
**Consequência prática**: `bras_baixados` **não** mede progresso — subestima. A
medida autoritativa é *bucket ∪ fila local em `~/Downloads`*:
```
# handles com preservação no bucket (do aws s3 ls --recursive)
awk '... itens/<handle>/<arquivo!=metadata.json, sem "thumb">' → 8.080
# handles de preservação ainda na fila local
ls ~/Downloads | grep '^BRAS__' | grep -v thumb | sed -E 's/^BRAS__(.+)__.*/\1/' → 7.271
# união → 12.986 de 13.059 (faltam 73, listados em pipeline/faltantes-preservacao.jsonl)
```
Isso também refaz a conta do achado 2.15: o risco de recolar o script não é
"85 itens", é **re-baixar ~950+** que o contador esqueceu (ou tudo, se o
storage estiver mesmo zerado).
## Concluído agora
- [x] **Thumbnails contaminados com bitstream de LICENÇA** (achado 2.8b): 2.653 arquivos locais apagados de `~/Downloads`, 146 já subidos removidos do bucket, `subidor.py` blindado com validação de magic bytes (rejeita e apaga automaticamente qualquer coisa que não comece com JPEG/PNG, mesmo que o nome diga o contrário). `baixador-thumb.js` v2 escrito com validação + fallback — falta só reconectar o navegador pra trocar o script rodando.
- [x] **WordPress → .md**: 636/637 posts colhidos (1 erro), exportados e convertidos — `wordpress/{id}-{slug}.md` no repo (front-matter YAML: id/url/título/autor/data/categorias/handles referenciados) + `wordpress-posts.jsonl` e a pasta `wordpress/` subindo pro bucket agora.
- [x] **Bug de corrida em arquivo temporário**: `baixador.py` e `subidor.py` moram na mesma pasta e usavam o mesmo nome fixo (`.tmp/metadata.json`) — rodando ao mesmo tempo, um apagava o arquivo que o outro tinha acabado de criar. Derrubou o `baixador.py` de verdade (crash, não só log de erro). Corrigido: nome agora inclui o PID. 3 itens (`1081,1082,1084`) já tinham subido um HTML de visualizador como se fosse "legado" antes da correção — limpo do bucket, metadata regenerado.
- [x] **PDFs de álbum perdidos no objdigital**: 44 itens (achado 2.9 do PLANO.md) tinham PDF disponível mas a extração pegava um `.htm` de landing page em vez do PDF de verdade. Corrigido em `catalogo.jsonl` e no IndexedDB antes do `baixador.py --somente-legado` alcançar esses handles (confirmado: nada contaminado no bucket). `baixador.py` reiniciado (PID 37909) com o catálogo corrigido.
## Concluído nesta sessão (chrome-extension-connection, ~17h UTC)
- [x] `pipeline/reparo-hash-url.js` colado e rodado — 78/78 corrigidos no IndexedDB
- [x] **Achado 2.13 (novo)**: mesmo bug do `#` reaparecia no redirect do DSpace (forma antiga → canônica). Fix: usar `/bitstream/handle/PREFIX/ID/arquivo` direto. `preservation_url` dos 78 reescrito de novo com a forma canônica.
- [x] Os 78 itens baixados cirurgicamente — **concluído 78/78, 0 erros** (script à parte, não o `BAIXA.retomar()` geral — aba antiga com o loop principal continua rodando sozinha, ver nota de handoff acima)
- [x] `BAIXA_THUMB` v2 iniciado (aba nova, não foi possível parar a v1 na aba antiga — ver nota acima; sem risco de dado ruim no bucket graças à validação de magic bytes já no `subidor.py`)
- [x] **Thumbnails v2: CONCLUÍDO 100%** — `BAIXA_THUMB.retomar()` rodado (2026-09-11 ~08h local) recuperou todos os 1.281 erros da queda de rede (13.059/13.059 ok, 0 erro permanente)
- [x] `baixador.py --somente-legado` reiniciado uma vez (2026-09-11 ~08h local, PID 88387) depois que a passada anterior terminou (2.113 ok, 589 erro de 2.702 processados) — idempotente, só reprocessa não-'ok'
- [x] Checagem de status a cada 30 min retomada nesta sessão (ver `/loop`)
## Mural (`web/wall.html`) — sessão `fix-mobile-wall-click-layout`, 2026-09-12
No ar em <https://xn--2dk.xyz/brasiliana/>. Commits em `rafapolo/xyz`:
`d8c3364` e `a6085fa` (empurrados pela sessão paralela `brasiliano-ae`, contêm
trabalho das duas) e `f645008`. O mural é publicado copiando
`web/wall.html` → `xyz/brasiliana/index.html` e `web/atlas/atlas-map.json` →
`xyz/brasiliana/atlas/atlas-map.json`.
### Achado 3.1 — a metade descritiva do catálogo nunca chegou ao mural
Buscar "indigenista" não achava nada, mesmo com 42 peças classificadas
`dc.subject = Política indigenista`. A busca livre estava certa (varre todo o
`meta` recursivamente); quem descartava era o **`summarize()` do
`web/build-atlas.js`**, que projetava 8 campos e jogava fora assunto,
descrição, lugar, fonte, editor, relação e código de arquivo.
Havia um segundo bug atrás: `pickDC()` devolve só a **primeira** ocorrência do
campo. Como o catálogo repete `dc.subject` (78.835 linhas para 13.060
registros, ~6 por item), mesmo incluindo o campo sobreviveria um assunto por
peça. O `pickAll()` novo coleta todas, deduplicadas, na ordem.
Regras que ficaram (espelhadas em `pipeline/reprojeta-meta-mapa.mjs`):
- entram: `dc.subject`, `dc.description`, `dc.coverage`, `dc.source`,
`dc.publisher`, `dc.relation`, `dc.identifier`
- ficam de fora: `dc.language` (é sempre `"por"` — casaria ~2.000 itens por
acidente, já que a busca é por substring) e `dc.format` (`"JPG"`)
- `dc.identifier` e `dc.relation` entram **sem as URLs**: era 1,1 MB de texto
que ninguém busca, escondendo o que presta — os códigos de arquivo
(`013RJ012036`) e os nomes de coleção (`Gilberto Ferrez`, `Diários
Associados`)
- `handle` e `source_url` **saíram** do `meta`: o primeiro é a chave sob a qual
o item já está arquivado no mapa, o segundo é o mesmo endereço para todos com
o handle no meio. O mural remonta a URL numa linha (`sourceUrl()`) e injeta o
handle na palha da busca, então buscar `5546` continua achando a peça.
Verificado no navegador: `indigenista` → 42, `Gilberto Ferrez` → 2.120,
`013RJ012036` → 2, `amazônia` → 156, `5546` → 1. E o ruído sumiu: `show=full`
e `brasilianafotografica` saíram de 12.552 para 0.
### Achado 3.2 — não vale pré-comprimir o `atlas-map.json`
Medido, não chutado:
- **No fio já vem gzipado.** `curl -sI` em <https://xn--2dk.xyz/brasiliana/atlas/atlas-map.json>
responde `content-encoding: gzip`. Pré-comprimir só moveria os mesmos bytes,
ao custo de um `DecompressionStream` no carregador e de um arquivo ilegível.
(O host não oferece brotli, e `DecompressionStream` não decodifica brotli.)
- **No repositório também.** O git comprime todo blob com zlib no pack, então
um JSON de 8 MB não ocupa 8 MB lá dentro — ocupa aproximadamente o que o
`.gz` ocuparia, e ainda dá diff.
Encolher o **conteúdo** foi o que valeu, e só até certo ponto — o gzip come
quase todo o ganho estrutural:
| variante | cru | gzip |
|---|---|---|
| com os campos descritivos | 9,61 MB | 1,11 MB |
| **sem `source_url` e `meta.handle`** (feito) | **8,00 MB** | **1,05 MB** |
| + dicionário nos campos de vocabulário pequeno | 6,96 MB | 0,99 MB |
| + dicionário de assuntos/lugares/descrições | 5,14 MB | 0,93 MB |
As duas últimas linhas quase dobram a economia crua e rendem 30-60 KB no fio
cada, em troca de uma camada de dicionário no carregador — troca ruim. Onde o
tamanho cru ainda pesa é o `JSON.parse` no celular, não o download. Referência:
antes de tudo isso o mapa era 5,52 MB / 0,73 MB.
### Consertos de interação (todos verificados por CDP num Chrome real)
- **Toque numa peça abria e fechava a ficha no mesmo gesto** (metade de cima da
tela). Um toque dispara `pointerup` e depois um `click` de compatibilidade,
que caía no backdrop recém-aparecido; a guarda de 400 ms usava
`stopPropagation()`, que **não alcança listeners do mesmo nó** — e quem fecha
a ficha é um listener do próprio `#card-wrap`. Agora é
`stopImmediatePropagation()`.
- **Tela inicial "sem estilo"**: `#title-screen` era irmão flex do `<p>` e,
invisível, ainda tomava metade da linha; e a regra `#loading p` pegava também
o subtítulo, jogando-o em maiúsculas com tracking. Virou `position:absolute` e
`#loading-msg`.
- **Painel de filtros fugia do dedo** no arrasto da década: o chip do filtro
aparecia e inseria uma linha inteira acima do corpo; o badge de contagem
crescia o header. Chips agora só com o painel fechado, badge reserva a caixa
com `visibility`.
- **Movimento da parede**: a câmera mirava o *alvo* enquanto estava atrás dele
(inclinava a vista a cada arrasto — era isso que fazia a parede "nadar"); o
easing era fração por frame (a 120 Hz convergia 2× mais rápido que a 60).
Agora constante de tempo, `lookAt` removido, e soltar em movimento joga a
parede com desaceleração. Zoom do scroll virou multiplicativo.
- **Back/forward do navegador** andam pelas peças abertas, com `#handle` na URL
(link direto reabre a peça). Mudar um filtro recolhe a ficha com **um** passo
de histórico, mesmo com `apply()` rodando a cada frame do slider.
- **Filtros alcançáveis com a peça aberta**: o painel estava em `z-index: 15`
debaixo do `#card-wrap` (20). Agora 25 (scrim 24), e no celular a ficha
estaciona acima do peek da sheet para não cobrir ‹ anterior / próxima ›.
### Achado 3.5 — o mural entrava em loop de carregamento no celular
Relato do usuário (14/09): no celular a tela inicial ficou em loop. Causa, lida
no carregador: o celular **decodificava cada página em 8192×8192** (~268 MB) e
só então reduzia para 4096 num canvas, retendo os canvases além das texturas —
pico de ~800 MB. O iOS não lança erro por falta de memória: mata a aba e a
**recarrega sozinho**, e a página recomeçava o mesmo carregamento. Não havia
tratamento de erro nenhum: fetch falho ou decodificação recusada deixavam a
barra parada para sempre.
Correção (commit `xyz` e `brasiliano` de 14/09):
- **Páginas leves** `atlas-m-0..3.webp` (3072², peça de 48 px), geradas pelo
`build-atlas.js` (`--so-mobile` regera só elas) e registradas em
`atlas-map.json` no campo `mobile`. Mesma posição de cada peça, então a conta de
uv não muda. 6,8 MB no total contra 42 MB; 36 MB decodificados por página.
- **Modo leve** em toque, `maxTextureSize` < 8192 ou `deviceMemory` ≤ 4, ou
forçado com `localStorage.mural_leve = '1'`.
- **Guarda contra loop**: marca `mural_carregando` em `sessionStorage` antes da
parte pesada, apagada só depois de dois quadros renderizados. Se a página abre
com a marca, a tentativa anterior morreu → vai direto para o modo leve e
mostra "modo leve — a tentativa anterior não terminou".
- **Nunca recarrega sozinho**: HTTP de erro, decodificação recusada e
`webglcontextlost` mostram aviso com botão "tentar de novo" (que recarrega uma
vez, já no modo leve). Progresso parado por 30 s mostra "o carregamento parou
de avançar" sem derrubar o que ainda pode estar vivo.
Verificado com Chrome real via `puppeteer-core` (perfil próprio, WebGL
swiftshader), emulando celular com toque — roteiros em
`scratchpad/teste-mural/testa.mjs` e `testa2.mjs`:
| cenário | resultado |
|---|---|
| A — celular carrega | só as 4 páginas leves, capa some em 8,3 s, marca apagada |
| C — marca de crash no desktop | aviso de modo leve, nenhuma página pesada, sem recarga |
| D — página falha no download | aviso com botão, **nenhuma recarga em 10 s** |
| E — botão tentar de novo | recarrega uma vez, termina no modo leve, sem 2ª recarga |
| F — perda de contexto WebGL | aviso com botão, sem recarga |
| G — download travado | aviso "parou de avançar" em 33 s, sem recarga |
**Verificado em produção** (14/09, após o deploy do GitHub Pages levar 62 s):
o cenário A contra `https://xn--2dk.xyz/brasiliana/` baixou só as 4 páginas
leves, a capa sumiu em 9 s pela rede real, sem falha, e a marca foi apagada.
O card no modo leve recorta a peça certa: a mesma peça tirada do atlas normal e
do leve difere 4/255 em média; a vizinha, 41,5/255.
**Limite do teste**: não há iPhone de verdade aqui — o limite de memória do iOS
não se emula no Chrome. O que está provado é que o celular não decodifica mais
8192² e que nenhum caminho de falha recarrega sozinho.
**Armadilha de teste**: `framenavigated` do Puppeteer conta a `about:blank`
inicial e mudanças de histórico do mural, então contar navegações dá falso
positivo de recarga. Usar uma marca em `window`, que só some numa recarga real.
## Miniaturas derivadas + reconciliação do ledger — 2026-09-12
Mural no ar com **13.059 peças** (antes 12.552): commits `bba32f6`,
`6a82a5e`, `1ecded1` e `e930158` em `rafapolo/xyz`. Desta vez o `atlas/` inteiro foi junto, não só o
`atlas-map.json` — o formato do map mudou (ver achado 3.4).
**Números fechados em 13/09 (medidos contra catálogo × ledger):**
| | |
|---|--:|
| itens no catálogo | 13.060 |
| com imagem de preservação | 12.991 |
| com thumbnail do DSpace | 13.059 |
| com miniatura derivada (`derivados/thumb256/`) | 13.059 |
| **no mural** | **13.059** |
| fora do mural | **1** — só `7523` |
Os 8 que faltavam (`7802`, `7814`, `7817`, `7848`, `7852`, `7861`, `7875`,
`8075`) entraram: eles **têm** `thumbnail/thumb_3.jpg` de imagem real no
bucket (109–250 KB). A afirmação anterior de que o thumbnail deles era texto
de licença estava errada — o texto era o `thumb_2` do material antigo do
finland, não o objeto do bucket.
Os 73 faltantes de preservação (achado 2.17/2.18) **não** ficam de fora do
mural: têm thumbnail do DSpace, que basta para o tile de 128px. Faltar
preservação e faltar no mural são coisas diferentes.
- [x] **Miniaturas derivadas — 13.051 itens (99,93%)**
em `s3://brasiliana/derivados/thumb256/` (256px WebP q80, ~8 KB cada,
100 MB no total). Prefixo `derivados/` deliberadamente fora de `itens/`:
`itens/` é material de origem, `derivados/` é reconstruível do zero.
Misturar os dois apagaria a procedência que o MANIFEST garante.
Composição final (medida, igual à do MANIFEST): **12.849 derivadas da
imagem de preservação** + **202 derivadas do `thumbnail/` do DSpace**
(itens com miniatura mas sem preservação coletada). Caminho até lá:
6.338 no finland a partir do bucket, 6.483 locais a partir de
`~/Downloads`, 164 do DSpace, e +66 do material do achado 2.21.
- [x] **MANIFEST.md atualizado e subido** — seção `derivados/`, os achados
2.19 e 2.20 abaixo, e a nota de que `thumbnail/` do DSpace não é
miniatura (achado 3.3). Re-subido em 12/09 com 13.051 / 99,93%, os 9
ausentes nomeados e a divergência 13.058 × 13.060 anotada.
- [x] **`build-atlas.js` parou de descartar item sem título** — a guarda
`if (!o.title) continue` escondia do mural `11749` (Museu Aeroespacial) e
`6241` (Instituto Moreira Salles), que têm imagem; o card já cai pra
"(sem título)" sozinho. 12.983 → 12.985.
- [x] **Wordmark "Acervo Brasiliana" ganhou fundo preto** — só tinha
`text-shadow`, que some sobre peça clara, e peça clara é metade do
acervo. Mesmo preto da dica do rodapé.
- [x] **Rótulo de contagem deixou de dizer "no acervo"** — dizia
"N peças no acervo" como se o mural fosse o acervo inteiro; agora diz
"peças com imagem". Foi essa frase que levantou a dúvida de que faltavam
miniaturas.
- [x] **`web/build-atlas.js` ganhou `--thumbs=DIR`** — a fonte antiga
(`~/Downloads`) é varrida pelo `subidor.py` conforme sobe, então nunca
tem o acervo inteiro ao mesmo tempo. Agora a fonte é reconstruível do
bucket.
- [x] **Ledger `subidor.sqlite` reconciliado com o bucket**: 28.553 linhas =
28.553 objetos, zero divergência nos dois sentidos. Backups em
`/private/tmp/claude-501/.../scratchpad/subidor.sqlite.bak-*`.
- 145 linhas de `thumbnail` sem objeto no bucket — são os bitstreams de
LICENÇA do achado 2.8b, removidos do bucket mas deixados no ledger
(426–815 bytes, só **5 sha256 distintos** entre os 145). Apagadas.
- 3 objetos no bucket sem linha nenhuma — inseridos, sha256 recalculado
do próprio objeto.
- 2.137 objetos `legado/` que **nenhum** ledger de upload conhecia: o
`baixador.py` só anota no `baixador.sqlite`. Importados, para que
`subidor.sqlite` seja ledger único.
- 13.051 derivados registrados como `tipo='derivado-thumb256'`.
- 63 "órfãos" de preservação/thumbnail eram **falso positivo**: gravados
durante a própria listagem do bucket. Conferidos no S3, estão lá.
### Achado 2.24 — 16 threads estouram o OpenSSL do macOS (SIGSEGV)
O `subidor-paralelo.py` morreu com **segmentation fault** no arranque da
passada das 10:56 de 13/09, e por isso **nada subiu durante horas** (o
`vigia-30min` só tentaria de novo meia hora depois). O relatório do macOS
aponta a pilha dentro do `libcrypto`:
```
libcrypto.3.dylib ossl_x509_store_ctx_get_by_subject
libcrypto.3.dylib X509_STORE_CTX_get1_issuer
libssl.3.dylib ssl_verify_internal / tls_post_process_server_certificate
```
É corrida de thread na verificação de certificado TLS quando muitas threads
fazem handshake ao mesmo tempo — bug do OpenSSL, não do nosso código. Com 4
threads subiu 50 arquivos a 19 GB/h sem problema. **Default baixado para 6
threads** no `subidor-paralelo.py`, no `vigia-30min.sh` e no
`rodar-paralelo.sh`. Não vale voltar a subir: acima de ~8 o ganho é nulo (o
link satura) e o risco de crash volta.
### Achado 2.25 — 75 itens com HTTP 500 persistente na origem (não é só o 7523)
Passada de 13/09 com a aba conectada: **108 imagens novas baixaram**, e **74
falharam com erro que não passa** — 45 × HTTP 500, 26 de rede e 3 de blob
curto, com retry sem recuperar nenhuma. Somando o `7523`, são **75 itens**
cujos bitstreams de preservação a origem não serve. Lista em
`pipeline/falhas-500-2026-09-13.txt`.
Isso substitui o achado 2.23 (que falava em 211 faltantes): os outros 136
baixaram. Nenhum dos 75 fica fora do mural — todos têm thumbnail do DSpace.
Diagnóstico que vale registrar: os 500 são **do item, não do ritmo**. Com 3
requisições simultâneas, `6545` e `6555` davam 500 enquanto `1014`, `8128`,
`10039`, `14358` e `6520` davam 200 no mesmo instante, inclusive em série com
3 s de espaçamento. E os itens ruins são vizinhos de id, então em ordem
crescente eles davam erros seguidos e disparavam o aborto automático — por
isso o motor passou a **embaralhar** a lista.
### Achado 2.26 — nova rodada sobre os 75 (13/09 noite): o que recupera e a armadilha da sequência
**Recuperados (3):** `6433` e `6520` por curl — a origem serve a ~11 KB/s, e o
"erro de rede" do navegador era estouro de tempo; `6656` no navegador na
**sequência 3** (5,4 MB, 1604×2612) — o catálogo apontava a sequência 2, que é
o texto de licença.
**Curl não escala:** depois dos 2 primeiros o Cloudflare devolveu `403`
(5.484 bytes) para 23 seguidos. Parado para não piorar o bloqueio.
**Armadilha — não aceitar "qualquer imagem em qualquer sequência".** A 1ª
versão do motor que varria as sequências 1–6 trouxe 8 "sucessos"; **7 eram o
`thumbnail/thumb_3.jpg` que já estava no bucket** (sha256 idêntico, 27–170 KB,
~800 px). Retidos antes de subir e descartados. A estrutura típica de um item
DSpace deste acervo:
| sequência | conteúdo |
|--:|---|
| 1 | imagem de preservação (nos itens com defeito: `HTTP 500`) |
| 2 | bitstream de licença (texto, 426–821 bytes) |
| 3 | thumbnail gerado pelo DSpace |
| 4–6 | inexistente |
A versão corrigida calcula SHA-256 no navegador e rejeita qualquer candidato
cujo hash já esteja no ledger como `thumbnail` ou `legado`, e grava com prefixo
`HOLD__` (o `subidor-paralelo.py` ignora) para conferência antes de subir.
### Achado 2.27 — os itens do IMS morrem num corte de 300 s; o `500` é o mesmo por sequência e por nome
Terceira rodada sobre os 75 (13/09, 21h), com um motor que registra o tempo de
cada tentativa. Três comportamentos distintos, todos da origem:
- **Corte em ~300 s sem resposta** (`rede@301s`), em 12 itens do Instituto
Moreira Salles (`8145`–`8316`). A requisição não recebe nem cabeçalho: um
teste com `Range: bytes=0-1023` estourou 18 s sem resposta, então não é
transferência lenta que cai no meio — o servidor trava antes de responder e
algo no caminho corta em 5 min. Retomar em pedaços não ajuda.
- **Corte em ~32 s** em 3 itens (`8271`, `8275`, `8277`): mesma família, limite
diferente.
- **`500` imediato** (0 s) em 11 itens (`8739`–`9148`). A forma canônica
`/bitstream/handle/20.500.12156.1/<id>/<arquivo>` — que busca pelo nome em
vez da sequência — **também devolve `500` em 0,2 s** (`8739`, `6545`). O
defeito está no servidor localizar o bitstream, não no endereçamento.
Estrutura das sequências nos itens do IMS: **1 = preservação, 2 = thumbnail**
(sem bitstream de licença, ao contrário dos do Museu Aeroespacial/Arquivo
Nacional/Fiocruz, onde 2 é licença e 3 é thumbnail). Varrer 1–6 nesses itens só
gasta tempo: 3–6 não existem.
Recuperado nesta rodada: `8396` (Biblioteca Nacional, sequência 3, 13,5 MB,
3361×4453) — mesmo padrão de `6656` e `7939`, onde o catálogo aponta a
sequência 2 (licença) e a imagem está na 3.
### Achado 2.28 — a cópia legada: só existe para a Biblioteca Nacional, e 25 "cópias" são páginas HTML
Medido em 14/09. A cópia legada vem do `objdigital.bn.br` e **só existe para
itens da Biblioteca Nacional**: 2.702 dos 13.060 têm `legacy_url`, todos da BN.
Os 16% de cobertura não são falta — os outros 10.358 itens não têm cópia legada
na origem.
| | |
|---|--:|
| elegíveis (`legacy_url` no catálogo) | 2.702 |
| no bucket como `legado/` | 2.137 |
| — imagens JPG | 1.899 (859 MB) |
| — PDF de álbum | 213 (8,3 GB) |
| — **páginas HTML** | **25** (~2,4 KB cada) |
| HTTP 404 permanente | 565 (499 jpg, 41 html, 22 pdf, 2 htm, 1 sid) |
**As 25 páginas HTML não são imagem.** São o visualizador Seadragon do
objdigital (`viewer.openDzi("GeneratedImages/dzc_output.xml")`): a imagem mora
numa pirâmide de tiles Deep Zoom ao lado da página. Itens: `586`, `587`, `630`,
`802`–`806`, `808`, `809`, `811`, `812`, `814`, `816`, `817`, `870`, `1080`,
`1083`, `1085`, `1086`, `3198`, `3545`, `3546`, `3866`, `6655`. **Todos têm
imagem de preservação no bucket**, então nenhum ficou sem imagem. Não foram
apagados — é decisão do usuário (ver pendência). Mesma família do bug de
`1081`/`1082`/`1084` (HTML de visualizador subido como legado), que foi limpo
na época; estes 25 escaparam.
**O objdigital não saiu do ar — mudou de endereço.** Em 14/09 a porta 80
fechou nos dois IPs (`200.9.175.46`, `177.223.196.160`), mas a 443 está aberta
e o site responde em **`https://objdigital.bn.br/objdigital2/acervo_digital/...`**
(o próprio visualizador Seadragon carrega o script desse prefixo). Prova de que
é a mesma fonte: 3 cópias legadas sorteadas do bucket, baixadas pelo endereço
novo, voltaram com **sha256 idêntico**. Correção de uma afirmação anterior
desta sessão: o `icon573843.jpg` usado como "controle" **não** tinha baixado
antes — ele é um dos 565 erros (item `10`), e responde `200` no endereço novo.
Consequência: **a maior parte dos 565 "404 permanentes" existe no endereço
novo.** Sondagem completa por `HEAD` em 14/09 (`pipeline/legado-caminho-novo.tsv`):
| no endereço novo | itens |
|---|--:|
| `200` com `image/jpeg` | 330 |
| `200` com `application/pdf` | 18 (o maior, 52 MB) |
| `200` com `text/html` (visualizador Deep Zoom, não arquivo) | 41 |
| `404` de verdade | 176 |
Recuperáveis como arquivo: **348** (~0,3 GB). As 41 páginas HTML somam-se às 25
já no bucket: **66 itens** cuja imagem só existe como pirâmide de tiles. A pirâmide Deep Zoom das 25 páginas HTML
também responde (item `3545`: `dzc_output.xml`, 5744×4245, tiles de 256 px).
**O `baixador.py` usa o endereço antigo (`http://…/acervo_digital/`) e precisa
ser atualizado antes de qualquer nova passada.
### Achado 2.22 — o gargalo do upload era o `subidor.py`, não o link; troca pelo paralelo
Eu tinha escrito aqui que o gargalo era o link de casa. **Errado.** Medido em
12/09 (`networkQuality`): **69 Mbps de upload (~31 GB/h)**, contra ~1,3 GB/h
reais. O `subidor.py` sobe um arquivo por vez, espera 1,5 s de "estabilidade" e
dispara **três** `aws s3 cp` por arquivo (arquivo, `metadata.json`, manifest),
cada um pagando a partida do aws-cli: ~5 s por arquivo, seja miniatura de 74 KB
ou preservação de 40 MB.
Além de lento, ele estava estragando duas coisas:
- **873 manifests com linha errada**, `derivado-thumb256/<handle>.webp`. É
consequência de eu ter registrado os derivados no ledger: ele lista toda
linha do handle, e esse caminho não existe em `itens/<handle>/`.
- **Duplicatas `(N)` voltando ao bucket** depois da limpeza do achado 2.20
(ex.: `5304`, `thumb_2.jpg` e `thumb_2 (1).jpg` com o mesmo sha256).
Substituto: `pipeline/subidor-paralelo.py` (boto3, threads, HEAD antes de apagar
o local, manifest uma vez por item, sem linha de derivado, duplicata e rejeito
registrados) + `pipeline/rodar-paralelo.sh`. Teste com boto3 contra o bucket:
PUT de 20 MB a 27 Mbps num único stream.
Testes antes de soltar, com o que cada um mostrou:
1. Lote de 30: todos eram rebaixa já presente no bucket e foram **apagados do
disco sem HEAD e sem registro**. Foi o que motivou a conferência por HEAD e a
tabela `descartados`. Para esses 30, a garantia é só o ledger, reconciliado
com o bucket às 12:50.
2. Lote de 150: 150 descartes, 150 conferidos no bucket.
3. Lote de 800: 319 enviados (319 conferidos, `image/jpeg`) + 481 descartes
conferidos; manifests dos 319 sem linha de derivado, sha do metadata batendo.
4. Primeira execução real (18:4x UTC) **interrompida**: o log mostrava TIFF
indo para `rejeitados/` (achado 2.19, correção). Assinaturas corrigidas; os 21
da quarentena (19 TIFF + 2 BMP) voltaram para a fila; relançado às 18:59.
Antes do relançamento, **180 arquivos locais (0,25 GB) já presentes no S3** foram
apagados do disco a pedido, cada um com MD5 local = ETag do objeto e registro em
`descartados`. Os outros 13.922 não estavam no bucket.
### Achado 2.23 — faltam 211 preservações depois da fila, não 73
Cruzando catálogo × (ledger ∪ fila) em 12/09: **12.849 / 13.060** itens terão
imagem de preservação quando a fila acabar (thumbnail: 13.059). Os 211:
- **71** da lista conhecida (`faltantes-preservacao.jsonl`, recusa da origem);
- **1** sem `preservation_url` no catálogo;
- **139** fora de qualquer lista. Exemplo: `10039`–`10049`, que no bucket têm só
`thumbnail/` e `metadata.json`. Desses: 8 rejeitados no upload antigo, 12 com só
`legado/`, ~119 sem rastro (nem bucket, nem fila, nem log).
Hipóteses, nenhuma confirmada: (a) o "12.986/13.059" do achado 2.16 contou
objetos `legado/` e `manifest` como preservação, porque o `awk` só excluía
`metadata.json` e thumbnail; (b) parte dos ~119 pode ser TIFF apagado pelo
`subidor.py` antigo antes do trecho que o `subidor.log` cobre.
### Achado 2.21 — a sessão de 11/09 já tinha as miniaturas no finland, e eu não olhei
`/root/brasiliana-wall/` no finland tinha, desde 11/09 13:17–13:41:
`downloads-flat/` (20.411 arquivos, todos `THUMB__`, 2,2 GB), `s3-thumbs/`
(5.168 handles do bucket, 851 MB) e o `web/atlas/` cujos quatro `.webp` batem
byte a byte com o que estava publicado no `xyz` até 12/09. **O mural de 12.552
peças foi construído lá**, a partir das miniaturas do DSpace — não de
`~/Downloads`, como o comentário do `build-atlas.js` leva a crer.
A união desses dois diretórios cobre **13.041 handles**, praticamente todos
JPEG válido. Eu puxei ~63 GB de imagens de preservação do bucket para derivar
12.985 miniaturas sem ver que 3 GB de miniatura já estavam no disco da mesma
máquina, cobrindo mais itens que o meu resultado. Aproveitados depois: +66
peças (74 no delta, 8 eram licença).
**Lição de runbook**: antes de gerar qualquer coisa no finland, `ls /root` e
`cat /root/.bash_history`. Sessões anteriores deixam material lá e nada no
projeto local registra isso.
Limpeza feita em 12/09: `/root/brasiliana-wall` e `/root/thumbs` apagados
(3,4 GB; disco 27 → 31 GB livres), depois de verificar que os 13.041 handles
estavam **todos** cobertos pelo bucket (7.038) ou pela fila do Mac (6.785) —
zero cópias exclusivas. Ficaram de pé, de propósito: `/root/.s3env` (600, de
08/09, anterior às nossas sessões — pode ser setup permanente da máquina) e
`/root/brasiliana/` (44 MB, `baixador.py` + catálogo + sqlite com 13 linhas).
### Achado 2.19 — a contaminação de LICENÇA também atinge a preservação, não só a miniatura
O achado 2.8b tratou isso como problema da trilha de thumbnail. Não é: os
handles **`20.500.12156.1/6656`** e **`20.500.12156.1/7939`** têm, em
`~/Downloads`, um "imagem.jpg" que é na verdade o bitstream de LICENÇA do
DSpace — 777 bytes de texto UTF-8 com CRLF e extensão `.jpg`. A validação de
magic bytes do `subidor.py` barra no upload, então **nada disso chegou ao
bucket**; o efeito é que esses dois itens não têm imagem coletada e ninguém
percebeu, porque a fila local os contava como prontos.
Implicação: qualquer contagem de "preservação pronta" que leia `~/Downloads`
pelo nome do arquivo está 2 itens otimista. A contagem pelo bucket está certa.
**Atualização 12/09**: não são 2, são 10. Ao derivar as miniaturas do material
do finland (achado 2.21) apareceram mais **oito** com o mesmo defeito, todos
na trilha de thumbnail: `7802`, `7814`, `7817`, `7848`, `7852`, `7861`,
`7875`, `8075`. São exatamente os itens que sobram fora do mural. O bloco
`78xx` sugere que a contaminação vem em lote, por faixa de handle — vale
varrer essa faixa inteira antes de dar o acervo por fechado.
**Correção 12/09 (noite) — a regra de validação estava errada.** O `subidor.py`
só aceitava JPEG/PNG e **apagava** o resto como "bitstream de licença". Pelo
próprio `subidor.log`, dos **40** arquivos que ele apagou assim, **só 1 era
texto de licença**: 36 eram **TIFF** (`II*\0`, ~8 MB, com extensão `.jpg`),
2 **BMP** e 1 **ZIP** (`001AAN014011.jpg.zip`, item `2384`). Resultado, item a
item (lista com URL em `pipeline/apagados-pelo-subidor-antigo.jsonl`):
- **8 itens ficaram sem nenhuma imagem de preservação**: `14358`, `14359`,
`14360`, `14361`, `14362`, `14363`, `14364`, `14368` (TIFF, série "Alb 0261").
- **18 itens** perderam só bitstreams extras (`extra_seqN`, TIFF), mas têm a
imagem principal no bucket ou na fila. `10993`/`10995` (BMP) e `2384` (ZIP)
voltaram por rebaixa e estão na fila.
O `subidor-paralelo.py` aceita TIFF/BMP/GIF/JP2/WebP pela assinatura e manda
qualquer outra coisa para `pipeline/rejeitados/` com linha na tabela
`rejeitados` — nunca apaga. Os 8 casos `78xx` de thumbnail com texto de licença
(acima) foram detectados pelo `vipsthumbnail`, não por essa regra, e continuam
valendo.
### Achado 2.20 — 3.511 duplicatas `(N)` no bucket, do Chrome rebaixando por cima
Objetos `arquivo (1).jpg` convivendo com `arquivo.jpg` byte a byte idêntico —
sufixo que o Chrome põe ao rebaixar um arquivo que já existe em `~/Downloads`,
e que o `subidor.py` subiu como se fosse nome legítimo. Confirmadas idênticas
por **ETag + tamanho** (não só tamanho) antes de apagar: 3.511 objetos,
0,39 GB, removidos do bucket e do ledger na mesma passada.
**Não** foram tocados, e precisam de decisão:
- **30 objetos** cujo gêmeo tem tamanho **diferente** — não são duplicata,
são versões distintas do mesmo nome. Precisam de conferência individual.
- **43 objetos** cujo nome tem `(N)` mas que são o **único** arquivo do item —
candidatos a renomear, nunca a apagar.
`aws s3api list-objects-v2` com `--query "Contents[].[Key,Size,ETag]"` resolve
isso sem baixar nada; o ETag é o MD5 pra upload simples (só os 2.612 objetos
multipart do bucket não servem pra comparação direta).
### Achado 3.3 — o `thumbnail/` do DSpace não é uma miniatura
Medido em amostra de 300: **721 a 3.709 px no maior lado, média 1.087**; menor
lado nunca abaixo de 462. São derivados de resolução média, não miniaturas de
listagem. Duas consequências:
1. Servem tão bem quanto a imagem de preservação pro tile de 128px do mural —
por isso deu pra cobrir os 164 itens sem preservação sem perda visível.
2. A linha "Miniatura | ~13.005 elegíveis" do MANIFEST descreve ~1 MB por
arquivo, não ~8 KB. Quem ler esperando miniatura vai baixar 100x mais do
que imagina.
### Achado 3.4 — publicar o mural agora exige mandar o `atlas/` junto
O `wall.html` de 12:32 passou a derivar `handle` (chave do map) e `source_url`
(mesmo prefixo pra todo item) em JS, em vez de lê-los de cada peça. O
`summarize()` do `build-atlas.js` acompanhou. Resultado: **8,2 MB de map com
12.983 peças contra 10 MB com 12.552** — menos peso com 431 peças a mais.
Mas isso quebra a nota antiga de que "o `atlas/` já está commitado lá e não
muda a cada alteração do mural": map e `wall.html` agora são um par acoplado
por formato. Publicar um sem o outro deixa o mural sem link e sem busca por
handle.
## Pendente
- [ ] **`dc.rights` ausente em 5.833 itens (44,7%)** — confirmado como característica real da fonte, não bug. Só precisa ficar documentado no MANIFEST final, sem ação de reparo.
- [ ] **`web/build-atlas.js` não está versionado em lugar nenhum** — `brasiliano/` não é repo git e o `xyz` só recebe o `index.html` e o `atlas/`. A correção do `summarize()`/`pickAll()` (achado 3.1) vive só no disco desta máquina; um `rm` leva junto. Mesma exposição vale para `pipeline/` inteiro.
- [ ] **Encolher mais o `atlas-map.json` só com dicionário** (achado 3.2): 8,00 → 5,14 MB crus por ~120 KB no fio. Só vale se o `JSON.parse` no celular virar problema medido — não antes.
- [ ] **30 objetos `(N)` com tamanho diferente do gêmeo** (achado 2.20) — conferência individual, não dá pra automatizar com segurança.
- [ ] **43 objetos `(N)` que são o único arquivo do item** (achado 2.20) — renomear tirando o sufixo, pra o nome não mentir sobre haver um original ao lado.
- [ ] **Handles `6656` e `7939` sem imagem de preservação** (achado 2.19) — rebaixar; o que há hoje na fila local é o bitstream de licença.
- [ ] **Conferir os códigos HTTP que nunca foram tabulados** — o runbook do achado 2.18 manda rodar `FALTA.erros()` no console e rotear por código (404 → reparar URL, 403 → bloqueio de origem, 500 → transitório, retomar). **Esse passo nunca foi executado**: o achado 2.18 concluiu "é recusa da origem" a partir do `status()` agregado, não da tabela de erros por handle. Sem isso não dá pra saber quantos dos 71 são reparáveis.
- [ ] **Reconferir o handle `7523` (HTTP 500)** — registrado uma vez e tratado como falha permanente da origem desde então, mas 500 costuma ser transitório e nunca houve segunda tentativa. **Não dá pra testar por `curl`, e agora está provado com controle** (12/09): com User-Agent, Accept, Accept-Language e Referer de Chrome, o `7523` devolve `403` de 5.484 bytes (página do Cloudflare) — e o item `10`, que sabidamente funciona e está no bucket, devolve **exatamente o mesmo `403` de 5.484 bytes**. É bloqueio geral a requisição sem sessão real, nada específico do `7523`. Precisa da aba; a extensão Claude-in-Chrome seguia desconectada em duas tentativas no mesmo dia.
- [ ] **Reconciliar 13.058 vs 13.060** — o MANIFEST fala em 13.058 itens reais, o `catalogo.jsonl` tem 13.060 handles distintos. Diferença de 2 nunca explicada; anotada no MANIFEST como pendência.
- [ ] **Depois que os 212 faltantes baixarem e subirem** (pedido do usuário, 13/09):
1. conferir se **todos** os itens têm miniatura em `derivados/thumb256/` e gerar
as que faltarem (as novas preservações não têm derivado ainda);
2. **regerar o mural 3D** com o conjunto completo — de preferência derivando
no finland direto do bucket (`itens/*/preservacao/*`), que é tráfego
intra-Hetzner, em vez de puxar pro Mac;
3. republicar `wall.html` + `atlas/` no `rafapolo/xyz` (par acoplado, achado 3.4).
- [ ] **Quando o `rodar-paralelo.sh` terminar** (log termina em `===== ... FIM`):
1. conferir no log as três fases sem `!!!`, a linha `FIM da fila` (enviados,
duplicatas, rejeitados, falhas) e os `pendente:` que ficaram no disco;
2. `ls pipeline/rejeitados/` + `select * from rejeitados` — só deve ter texto
de licença; imagem ali é bug de assinatura;
3. reconciliar ledger × bucket (mesmo diff do achado de 12/09: 0 divergências
nos dois sentidos) e amostrar manifests (`grep derivado` = 0);
4. varrer `~/Downloads`: apagar o que tiver MD5 = ETag no bucket (mesmo
método dos 180 de 12/09), manter o resto;
5. atualizar a tabela de contagens do MANIFEST com os números fechados.
- [ ] **Rebaixar o que o `subidor.py` antigo apagou** (achado 2.19, correção): 8 itens sem preservação (`14358`–`14364`, `14368`) + ~20 `extra_seqN` em TIFF — lista com URL em `pipeline/apagados-pelo-subidor-antigo.jsonl`. Precisa da aba do navegador.
- [x] ~~Recontar os faltantes de preservação (211)~~ — feito em 13/09: 136 baixaram, sobraram **75** com defeito de origem (achado 2.25), listados em `pipeline/falhas-500-2026-09-13.txt`. Script usado: `pipeline/baixar-faltantes-v3.js` (ou o motor `F5` colado direto na aba).
- [ ] **`subidor.py` antigo: aposentar ou corrigir a regra de assinatura** antes de qualquer uso futuro — do jeito que está, apaga TIFF.
- [ ] **Rebaixar os 69 que sobraram** (achados 2.25–2.27; 6 recuperados em 13/09 à noite) quando a origem se recuperar — todos têm thumbnail, falta a preservação. Um `FALTA3.retomar()`/`F5.iniciar()` na aba basta; o que já baixou fica marcado no `localStorage` (`bras_faltantes_v3`).
- [ ] **Handle `8396` sem imagem e sem diagnóstico** — não está em `pipeline/faltantes-preservacao.jsonl` nem nas exceções conhecidas do MANIFEST; apareceu ao cruzar catálogo × miniaturas. Precisa de uma tentativa avulsa pra saber se é recusa da origem (achado 2.18) ou outra coisa.
- [ ] **`.git` do `rafapolo/xyz` está em 2,5 GB** e cada publicação do atlas soma ~51 MB permanentes (binário não faz delta). Se o mural for republicar atlas com frequência, tirar os `.webp` do git — LFS, ou servir do próprio bucket, que já tem os thumbs em `derivados/thumb256/`.
- [ ] **Crawl da Fase 1 pode ter mais buracos** (achado 2.10 do PLANO.md): 2 itens reais (`1799`, `4798`) foram achados faltando no catálogo só porque o WordPress os citava — já corrigidos, mas não existe verificação exaustiva de que a enumeração por `browse?type=title` pegou 100% de cada comunidade. Sem ideia barata de auditar isso por completo agora.
- [ ] **Baixar os 348 legados recuperáveis pelo endereço novo** (achado 2.28) — atualizar antes o `baixador.py` para `https://objdigital.bn.br/objdigital2/acervo_digital/`.
- [ ] **Decidir o que fazer com as 66 páginas HTML de visualizador** (25 já em `legado/` + 41 no endereço novo; achado 2.28): apagar do bucket e do ledger (não são imagem), ou montar a imagem a partir dos tiles Deep Zoom quando o objdigital voltar. Todos os 25 itens já têm preservação.
## Ao final (não fazer antes do download terminar)
- [x] Gerar `MANIFEST.md` (o que é, como foi coletado, quando) — feito; atualizado em 2026-09-12 com `derivados/` e os achados 2.19/2.20/3.3
- [ ] Confirmar no bucket que a contagem de itens/checksums bate com os 13.058 itens reais esperados
## Notas técnicas úteis pra retomar
- Downloads automáticos do Chrome exigem permissão manual em `chrome://settings/content/automaticDownloads` (site adicionado: `https://brasilianafotografica.bn.gov.br`) — já concedida, vale pra novos scripts também, não só o original.
- `BRAS`, `BAIXA`, `BAIXA_THUMB`, `BAIXA_EXTRA` — todos no `window` da mesma aba, mesmo IndexedDB (`bras`). Todos sobrevivem a fechar/reabrir a aba (dado fica salvo), mas o *loop* para e precisa recolar o script certo pra retomar.
- **Regerar as miniaturas derivadas** (tudo reconstruível, nada precisa ser
guardado): no finland, `apt install libvips-tools parallel` + s5cmd; pra cada
chave de `itens/*/preservacao/*`, `s5cmd cat` num tmp e
`vipsthumbnail "$TMP" --size 256 --output "$OUT[Q=80,strip]"`, com
`parallel -j 10`. ~640 thumbs/min, tráfego intra-Hetzner (bucket é `hel1`, o
finland também) — não passa pelo link de casa. O que ainda está só em
`~/Downloads` sai mais barato derivado localmente com `magick`: mandar os
71 GB pro finland custaria os mesmos dias do upload pendente.
- **Cuidado ao parsear `s5cmd ls`**: nomes de arquivo do acervo têm espaço
**duplo** (`48094 Álbum 311 - ...jpg`). `awk '{$1="";$2="";$3=""; print}'
recolapsa esses espaços e produz chaves que não existem — 31 downloads
falharam assim antes de eu perceber. Use
`sed -E 's|^[0-9/]+ +[0-9:]+ +[0-9]+ ||'`, que preserva o resto literal.
- **`sqlite3` desta máquina tem `.sqliterc` forçando modo box**: qualquer
script que leia a saída precisa de `sqlite3 -init /dev/null -noheader
-separator $'\t'`, senão vem borda de tabela no lugar dos dados (me custou
um diff inteiro de falso positivo).
- **`until ! pgrep -f X; do sleep; done` nunca termina**: a linha de comando do
próprio loop contém `X`, então o `pgrep -f` se acha e a condição nunca vira
falsa. Um loop desses ficou girando horas depois do trabalho acabar (12/09,
morto à mão). Esperar por PID (`while kill -0 $PID`) ou usar `pgrep -f
'[t]humb-local'` para o padrão não casar consigo mesmo.
- **Parar processo lançado em background: SIGTERM, não SIGINT.** Processo
iniciado com `&` num shell não interativo herda o SIGINT ignorado (o
`subidor.py` ficou 60 s vivo depois do `kill -INT`). Com SIGTERM, o `aws s3
cp` filho termina órfão, o que é inofensivo: sem linha no ledger e sem
apagar o local, o próximo uploader sobe de novo a mesma chave.
- **Upload de casa: 69 Mbps** (`networkQuality -s`, 12/09); download 312 Mbps.
- **Assinaturas aceitas como imagem** (`subidor-paralelo.py`): JPEG `FFD8`, PNG,
TIFF `II*\0`/`MM\0*`, GIF, JP2, BMP `BM`, WebP. Content-Type sai do conteúdo,
não da extensão (TIFF chega com nome `.jpg`).
- **`vipsthumbnail --output` é relativo à pasta do arquivo de entrada**, não ao
diretório corrente: `vipsthumbnail tmp/x.img --output out/y.webp` tenta
gravar em `tmp/out/y.webp` e falha. Usar caminho absoluto na saída. (Custou
uma miniatura "refeita" que não foi refeita, em 13/09.)
- Scripts-fonte em `pipeline/`: `coletor.js` / `coletor-resume.js` (metadados), `baixador-navegador.js` (preservação, todos os itens), `baixador-thumb.js` (miniaturas), `baixador-extras.js` (2a+ imagem de itens multi-bitstream), `reparo-multibitstream.js` (enumera bitstreams reais de um item).