From f05cdc5b9d15205a707d20d7844b2182fa031856 Mon Sep 17 00:00:00 2001 From: rafapolo Date: Wed, 30 Sep 2026 22:32:36 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20TODO=20s=C3=B3=20com=20o=20que=20est?= =?UTF-8?q?=C3=A1=20aberto;=20vers=C3=A3o=20completa=20arquivada=20em=20ta?= =?UTF-8?q?sks/done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/TODO.md | 741 +------------------------------- tasks/done/TODO-2026-09-30.md | 776 ++++++++++++++++++++++++++++++++++ 2 files changed, 792 insertions(+), 725 deletions(-) create mode 100644 tasks/done/TODO-2026-09-30.md diff --git a/tasks/TODO.md b/tasks/TODO.md index 290eb97..069ae80 100644 --- a/tasks/TODO.md +++ b/tasks/TODO.md @@ -1,738 +1,29 @@ # 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//' → 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 . 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 - 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 `

` 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//` — 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/.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//`. -- **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. +> Só o que está **aberto**. O que foi concluído, com os achados e o histórico +> das sessões, está em [`tasks/done/`](done/); a última versão completa deste +> arquivo, com os achados 2.15–2.28 e 3.1–3.5 citados abaixo, é +> [`done/TODO-2026-09-30.md`](done/TODO-2026-09-30.md). Plano, estado e +> descrição NOBRADE: [`PLANO.md`](PLANO.md). Scripts e estado em `pipeline/`. ## 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. +### Acervo no bucket +- [ ] **Rebaixar os 69 itens sem preservação** (achados 2.25–2.27): a origem responde HTTP 500 ao bitstream; todos têm miniatura. Quando a origem se recuperar, basta um `FALTA3.retomar()`/`F5.iniciar()` na aba do navegador (o que já baixou fica marcado em `bras_faltantes_v3`). Lista em `errors.md`. +- [ ] **30 arquivos `extra_seqN` apagados pelo `subidor.py` antigo** (achado 2.19): os 8 itens principais já têm preservação de novo, mas estes 30 extras (vários TIFF) não aparecem no ledger. Lista com URL em `pipeline/apagados-pelo-subidor-antigo.jsonl`. Precisa da aba do navegador. +- [ ] **`subidor.py` antigo: aposentar ou corrigir a regra de assinatura** antes de qualquer uso futuro — do jeito que está, apaga TIFF. O que roda hoje é o `subidor-paralelo.py`. - [ ] **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. +- [ ] **Confirmar no bucket que a contagem de itens/checksums bate** com os itens reais esperados. +- [ ] **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. +- [ ] **Crawl da Fase 1 pode ter mais buracos** (achado 2.10 do PLANO.md): 2 itens reais (`1799`, `4798`) só foram achados porque o WordPress os citava. Não existe verificação exaustiva de que `browse?type=title` pegou 100% de cada comunidade. +- [ ] **`dc.rights` ausente em 5.833 itens (44,7%)** — característica real da fonte, não bug. Só precisa ficar documentado no MANIFEST final. -## 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 +### Mural +- [ ] **`.git` do `rafapolo/xyz` está em 2,5 GB** e cada publicação do atlas `.webp` soma ~51 MB permanentes (binário não faz delta). Se o atlas for republicado com frequência, tirar os `.webp` do git — LFS, ou servir do próprio bucket. (Publicar só `index.html` + `atlas-map.json`, como em 30/09, não pesa.) +- [ ] **Encolher o `atlas-map.json` com dicionário** (achado 3.2) — hoje 10,4 MB com os campos NOBRADE. Só vale se o `JSON.parse` no celular virar problema medido. ## Notas técnicas úteis pra retomar diff --git a/tasks/done/TODO-2026-09-30.md b/tasks/done/TODO-2026-09-30.md new file mode 100644 index 0000000..290eb97 --- /dev/null +++ b/tasks/done/TODO-2026-09-30.md @@ -0,0 +1,776 @@ +# 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//' → 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 . 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 + 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 `

` 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//` — 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/.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//`. +- **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).