Files
brasiliano/tasks/TODO.md
T
rodado fc45ccd9d0 fix: carregador do mural à prova de loop no celular
build-atlas.js gera páginas leves de 3072 px para celular (--so-mobile
regera só elas) e registra o campo mobile no atlas-map.json.

wall.html usa essas páginas em toque ou GPU/aparelho fraco, detecta pela
marca em sessionStorage que a tentativa anterior morreu e força o modo leve,
e trata falha de download, de decodificação e perda de contexto WebGL com
aviso e botão, sem recarregar sozinho. Detalhes e testes no achado 3.5.
2026-09-14 12:37:59 +02:00

51 KiB
Raw Blame History

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)

  • 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.

  • Thumbnails — ✅ concluído 100% (13.059/13.059). retomar() já foi rodado uma vez (08h local); não re-rodar.
  • 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.
  • subidor.py substituído pelo subidor-paralelo.py em 2026-09-12 (achado 2.22); o texto a seguir é histórico. (pipeline/, --vigiar, PID 97295) — vivo há 1d8h, vigiando ~/Downloads. 2026-09-11 ~20h UTC: fila local de 19.897 arquivos BRAS__ / ~77 GB (12.511 thumb, 1.660 __imagem, 5.726 com nome original). Bucket: 33.388 objetos / 60,3 GB, 8.080 itens com metadata.json (61,9% dos 13.059). 2026-09-12 09:45 UTC: 16.207 enviados, fila de 18.286. Taxa nas horas acordadas ≈ 650–740 arq/h ≈ 2,0–2,4 GB/h.

⚠️ O relógio é de horas ACORDADAS, não de horas corridas. Os envios pararam às 22:30 UTC de 11/set e só voltaram às 09:00 UTC de 12/set — 10,5 h de silêncio, que é a máquina dormindo (00:30–11:00 local), não falha do subidor (o processo seguiu vivo e voltou sozinho). Por isso:

  • fila de 18.286 ÷ ~700 arq/h = ~26 h de máquina acordada
  • em dias normais (~14 h acordado), isso vira ~2 dias de calendário
  • com caffeinate -is segurando a máquina acordada, ~26 h corridas

Vale decidir isso conscientemente: é a diferença entre terminar em ~1 dia ou ~2.

Achado 2.15 — aba do coletor inalcançável, e storage da origem aparece vazio (2026-09-11 ~20h UTC)

Tentativa de retomar a trilha 1 sozinho falhou, mas rendeu dois fatos novos:

  1. Sobraram instâncias headless do Chrome de uma sessão anterior, uma delas lançada com o perfil padrão (--user-data-dir default). Mortas (PIDs 89329, 89327, 14818); o Chrome real do usuário (PID 57263, aberto desde 9/set) foi preservado. Nunca subir headless sem --user-data-dir próprio — disputa o perfil onde mora todo o estado do coletor.
  2. Numa aba nova em https://brasilianafotografica.bn.gov.br (mesmo perfil, mesma origem), localStorage devolve 0 chaves e indexedDB.databases() devolve [] — enquanto ler-localstorage.js, lendo o leveldb do disco do mesmo perfil/origem, devolve bras_baixados: 12.974. Ou seja: o contexto vivo e o disco discordam. Não dá pra BAIXA.retomar() numa aba nova: sem o IndexedDB bras, alvos() devolveria [].

Não colar baixador-navegador.js numa aba nesse estado: se o storage estiver mesmo zerado, FEITOS começa vazio e a fila vira os ~13 mil itens inteiros de novo (centenas de GB, e carga indevida na fonte). O caminho seguro é o usuário voltar na aba original do coletor (a que já tem BAIXA no window) e rodar BAIXA.status() / BAIXA.retomar() lá.

Achado 2.16 — bras_baixados anda PRA TRÁS; não usar como medida (2026-09-11 20:45 UTC)

O contador caiu de 12.974 → 12.111 entre 20:14 e 20:43 UTC (três leituras seguidas devolvem 12.111; o leveldb foi escrito pelo Chrome às 22:24 local, 19 min antes da leitura — é gravação real, não cache velho).

Causa provável: marcar() reescreve o array inteiro a partir do FEITOS carregado quando o script foi colado. Qualquer aba com snapshot velho que ainda marque um item sobrescreve a lista boa por uma mais curta. Com duas abas vivas (v1 e v2 conviveram), a lista é sempre a do último escritor, não a união.

Consequência prática: bras_baixados não mede progresso — subestima. A medida autoritativa é bucket ∪ fila local em ~/Downloads:

# handles com preservação no bucket (do aws s3 ls --recursive)
awk '... itens/<handle>/<arquivo!=metadata.json, sem "thumb">' → 8.080
# handles de preservação ainda na fila local
ls ~/Downloads | grep '^BRAS__' | grep -v thumb | sed -E 's/^BRAS__(.+)__.*/\1/' → 7.271
# união → 12.986 de 13.059 (faltam 73, listados em pipeline/faltantes-preservacao.jsonl)

Isso também refaz a conta do achado 2.15: o risco de recolar o script não é "85 itens", é re-baixar ~950+ que o contador esqueceu (ou tudo, se o storage estiver mesmo zerado).

Concluído agora

  • 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.
  • 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.
  • 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.
  • 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)

  • pipeline/reparo-hash-url.js colado e rodado — 78/78 corrigidos no IndexedDB
  • 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.
  • 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)
  • 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)
  • 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)
  • 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'
  • Checagem de status a cada 30 min retomada nesta sessão (ver /loop)

Mural (web/wall.html) — sessão fix-mobile-wall-click-layout, 2026-09-12

No ar em https://xn--2dk.xyz/brasiliana/. Commits em rafapolo/xyz: d8c3364 e a6085fa (empurrados pela sessão paralela brasiliano-ae, contêm trabalho das duas) e f645008. O mural é publicado copiando web/wall.html → xyz/brasiliana/index.html e web/atlas/atlas-map.json → xyz/brasiliana/atlas/atlas-map.json.

Achado 3.1 — a metade descritiva do catálogo nunca chegou ao mural

Buscar "indigenista" não achava nada, mesmo com 42 peças classificadas dc.subject = Política indigenista. A busca livre estava certa (varre todo o meta recursivamente); quem descartava era o summarize() do web/build-atlas.js, que projetava 8 campos e jogava fora assunto, descrição, lugar, fonte, editor, relação e código de arquivo.

Havia um segundo bug atrás: pickDC() devolve só a primeira ocorrência do campo. Como o catálogo repete dc.subject (78.835 linhas para 13.060 registros, ~6 por item), mesmo incluindo o campo sobreviveria um assunto por peça. O pickAll() novo coleta todas, deduplicadas, na ordem.

Regras que ficaram (espelhadas em pipeline/reprojeta-meta-mapa.mjs):

  • entram: dc.subject, dc.description, dc.coverage, dc.source, dc.publisher, dc.relation, dc.identifier
  • ficam de fora: dc.language (é sempre "por" — casaria ~2.000 itens por acidente, já que a busca é por substring) e dc.format ("JPG")
  • dc.identifier e dc.relation entram sem as URLs: era 1,1 MB de texto que ninguém busca, escondendo o que presta — os códigos de arquivo (013RJ012036) e os nomes de coleção (Gilberto Ferrez, Diários Associados)
  • handle e source_url saíram do meta: o primeiro é a chave sob a qual o item já está arquivado no mapa, o segundo é o mesmo endereço para todos com o handle no meio. O mural remonta a URL numa linha (sourceUrl()) e injeta o handle na palha da busca, então buscar 5546 continua achando a peça.

Verificado no navegador: indigenista → 42, Gilberto Ferrez → 2.120, 013RJ012036 → 2, amazônia → 156, 5546 → 1. E o ruído sumiu: show=full e brasilianafotografica saíram de 12.552 para 0.

Achado 3.2 — não vale pré-comprimir o atlas-map.json

Medido, não chutado:

  • No fio já vem gzipado. curl -sI em https://xn--2dk.xyz/brasiliana/atlas/atlas-map.json responde content-encoding: gzip. Pré-comprimir só moveria os mesmos bytes, ao custo de um DecompressionStream no carregador e de um arquivo ilegível. (O host não oferece brotli, e DecompressionStream não decodifica brotli.)
  • No repositório também. O git comprime todo blob com zlib no pack, então um JSON de 8 MB não ocupa 8 MB lá dentro — ocupa aproximadamente o que o .gz ocuparia, e ainda dá diff.

Encolher o conteúdo foi o que valeu, e só até certo ponto — o gzip come quase todo o ganho estrutural:

variante cru gzip
com os campos descritivos 9,61 MB 1,11 MB
sem source_url e meta.handle (feito) 8,00 MB 1,05 MB
+ dicionário nos campos de vocabulário pequeno 6,96 MB 0,99 MB
+ dicionário de assuntos/lugares/descrições 5,14 MB 0,93 MB

As duas últimas linhas quase dobram a economia crua e rendem 30-60 KB no fio cada, em troca de uma camada de dicionário no carregador — troca ruim. Onde o tamanho cru ainda pesa é o JSON.parse no celular, não o download. Referência: antes de tudo isso o mapa era 5,52 MB / 0,73 MB.

Consertos de interação (todos verificados por CDP num Chrome real)

  • Toque numa peça abria e fechava a ficha no mesmo gesto (metade de cima da tela). Um toque dispara pointerup e depois um click de compatibilidade, que caía no backdrop recém-aparecido; a guarda de 400 ms usava stopPropagation(), que não alcança listeners do mesmo nó — e quem fecha a ficha é um listener do próprio #card-wrap. Agora é stopImmediatePropagation().
  • Tela inicial "sem estilo": #title-screen era irmão flex do <p> e, invisível, ainda tomava metade da linha; e a regra #loading p pegava também o subtítulo, jogando-o em maiúsculas com tracking. Virou position:absolute e #loading-msg.
  • Painel de filtros fugia do dedo no arrasto da década: o chip do filtro aparecia e inseria uma linha inteira acima do corpo; o badge de contagem crescia o header. Chips agora só com o painel fechado, badge reserva a caixa com visibility.
  • Movimento da parede: a câmera mirava o alvo enquanto estava atrás dele (inclinava a vista a cada arrasto — era isso que fazia a parede "nadar"); o easing era fração por frame (a 120 Hz convergia 2× mais rápido que a 60). Agora constante de tempo, lookAt removido, e soltar em movimento joga a parede com desaceleração. Zoom do scroll virou multiplicativo.
  • Back/forward do navegador andam pelas peças abertas, com #handle na URL (link direto reabre a peça). Mudar um filtro recolhe a ficha com um passo de histórico, mesmo com apply() rodando a cada frame do slider.
  • Filtros alcançáveis com a peça aberta: o painel estava em z-index: 15 debaixo do #card-wrap (20). Agora 25 (scrim 24), e no celular a ficha estaciona acima do peek da sheet para não cobrir ‹ anterior / próxima ›.

Achado 3.5 — o mural entrava em loop de carregamento no celular

Relato do usuário (14/09): no celular a tela inicial ficou em loop. Causa, lida no carregador: o celular decodificava cada página em 8192×8192 (~268 MB) e só então reduzia para 4096 num canvas, retendo os canvases além das texturas — pico de ~800 MB. O iOS não lança erro por falta de memória: mata a aba e a recarrega sozinho, e a página recomeçava o mesmo carregamento. Não havia tratamento de erro nenhum: fetch falho ou decodificação recusada deixavam a barra parada para sempre.

Correção (commit xyz e brasiliano de 14/09):

  • Páginas leves atlas-m-0..3.webp (3072², peça de 48 px), geradas pelo build-atlas.js (--so-mobile regera só elas) e registradas em atlas-map.json no campo mobile. Mesma posição de cada peça, então a conta de uv não muda. 6,8 MB no total contra 42 MB; 36 MB decodificados por página.
  • Modo leve em toque, maxTextureSize < 8192 ou deviceMemory ≤ 4, ou forçado com localStorage.mural_leve = '1'.
  • Guarda contra loop: marca mural_carregando em sessionStorage antes da parte pesada, apagada só depois de dois quadros renderizados. Se a página abre com a marca, a tentativa anterior morreu → vai direto para o modo leve e mostra "modo leve — a tentativa anterior não terminou".
  • Nunca recarrega sozinho: HTTP de erro, decodificação recusada e webglcontextlost mostram aviso com botão "tentar de novo" (que recarrega uma vez, já no modo leve). Progresso parado por 30 s mostra "o carregamento parou de avançar" sem derrubar o que ainda pode estar vivo.

Verificado com Chrome real via puppeteer-core (perfil próprio, WebGL swiftshader), emulando celular com toque — roteiros em scratchpad/teste-mural/testa.mjs e testa2.mjs:

cenário resultado
A — celular carrega só as 4 páginas leves, capa some em 8,3 s, marca apagada
C — marca de crash no desktop aviso de modo leve, nenhuma página pesada, sem recarga
D — página falha no download aviso com botão, nenhuma recarga em 10 s
E — botão tentar de novo recarrega uma vez, termina no modo leve, sem 2ª recarga
F — perda de contexto WebGL aviso com botão, sem recarga
G — download travado aviso "parou de avançar" em 33 s, sem recarga

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.

  • 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.
  • 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.
  • 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.
  • 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é.
  • 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.
  • 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.
  • Ledger subidor.sqlite reconciliado com o bucket: 28.553 linhas = 28.553 objetos, zero divergência nos dois sentidos. Backups em /private/tmp/claude-501/.../scratchpad/subidor.sqlite.bak-*. - 145 linhas de thumbnail sem objeto no bucket — são os bitstreams de LICENÇA do achado 2.8b, removidos do bucket mas deixados no ledger (426–815 bytes, só 5 sha256 distintos entre os 145). Apagadas. - 3 objetos no bucket sem linha nenhuma — inseridos, sha256 recalculado do próprio objeto. - 2.137 objetos legado/ que nenhum ledger de upload conhecia: o baixador.py só anota no baixador.sqlite. Importados, para que subidor.sqlite seja ledger único. - 13.051 derivados registrados como tipo='derivado-thumb256'. - 63 "órfãos" de preservação/thumbnail eram falso positivo: gravados durante a própria listagem do bucket. Conferidos no S3, estão lá.

Achado 2.24 — 16 threads estouram o OpenSSL do macOS (SIGSEGV)

O subidor-paralelo.py morreu com segmentation fault no arranque da passada das 10:56 de 13/09, e por isso nada subiu durante horas (o vigia-30min só tentaria de novo meia hora depois). O relatório do macOS aponta a pilha dentro do libcrypto:

libcrypto.3.dylib  ossl_x509_store_ctx_get_by_subject
libcrypto.3.dylib  X509_STORE_CTX_get1_issuer
libssl.3.dylib     ssl_verify_internal / tls_post_process_server_certificate

É corrida de thread na verificação de certificado TLS quando muitas threads fazem handshake ao mesmo tempo — bug do OpenSSL, não do nosso código. Com 4 threads subiu 50 arquivos a 19 GB/h sem problema. Default baixado para 6 threads no subidor-paralelo.py, no vigia-30min.sh e no rodar-paralelo.sh. Não vale voltar a subir: acima de ~8 o ganho é nulo (o link satura) e o risco de crash volta.

Achado 2.25 — 75 itens com HTTP 500 persistente na origem (não é só o 7523)

Passada de 13/09 com a aba conectada: 108 imagens novas baixaram, e 74 falharam com erro que não passa — 45 × HTTP 500, 26 de rede e 3 de blob curto, com retry sem recuperar nenhuma. Somando o 7523, são 75 itens cujos bitstreams de preservação a origem não serve. Lista em pipeline/falhas-500-2026-09-13.txt.

Isso substitui o achado 2.23 (que falava em 211 faltantes): os outros 136 baixaram. Nenhum dos 75 fica fora do mural — todos têm thumbnail do DSpace.

Diagnóstico que vale registrar: os 500 são do item, não do ritmo. Com 3 requisições simultâneas, 6545 e 6555 davam 500 enquanto 1014, 8128, 10039, 14358 e 6520 davam 200 no mesmo instante, inclusive em série com 3 s de espaçamento. E os itens ruins são vizinhos de id, então em ordem crescente eles davam erros seguidos e disparavam o aborto automático — por isso o motor passou a embaralhar a lista.

Achado 2.26 — nova rodada sobre os 75 (13/09 noite): o que recupera e a armadilha da sequência

Recuperados (3): 6433 e 6520 por curl — a origem serve a ~11 KB/s, e o "erro de rede" do navegador era estouro de tempo; 6656 no navegador na sequência 3 (5,4 MB, 1604×2612) — o catálogo apontava a sequência 2, que é o texto de licença.

Curl não escala: depois dos 2 primeiros o Cloudflare devolveu 403 (5.484 bytes) para 23 seguidos. Parado para não piorar o bloqueio.

Armadilha — não aceitar "qualquer imagem em qualquer sequência". A 1ª versão do motor que varria as sequências 1–6 trouxe 8 "sucessos"; 7 eram o thumbnail/thumb_3.jpg que já estava no bucket (sha256 idêntico, 27–170 KB, ~800 px). Retidos antes de subir e descartados. A estrutura típica de um item DSpace deste acervo:

sequência conteúdo
1 imagem de preservação (nos itens com defeito: HTTP 500)
2 bitstream de licença (texto, 426–821 bytes)
3 thumbnail gerado pelo DSpace
4–6 inexistente

A versão corrigida calcula SHA-256 no navegador e rejeita qualquer candidato cujo hash já esteja no ledger como thumbnail ou legado, e grava com prefixo HOLD__ (o subidor-paralelo.py ignora) para conferência antes de subir.

Achado 2.27 — os itens do IMS morrem num corte de 300 s; o 500 é o mesmo por sequência e por nome

Terceira rodada sobre os 75 (13/09, 21h), com um motor que registra o tempo de cada tentativa. Três comportamentos distintos, todos da origem:

  • Corte em ~300 s sem resposta (rede@301s), em 12 itens do Instituto Moreira Salles (8145–8316). A requisição não recebe nem cabeçalho: um teste com Range: bytes=0-1023 estourou 18 s sem resposta, então não é transferência lenta que cai no meio — o servidor trava antes de responder e algo no caminho corta em 5 min. Retomar em pedaços não ajuda.
  • Corte em ~32 s em 3 itens (8271, 8275, 8277): mesma família, limite diferente.
  • 500 imediato (0 s) em 11 itens (8739–9148). A forma canônica /bitstream/handle/20.500.12156.1/<id>/<arquivo> — que busca pelo nome em vez da sequência — também devolve 500 em 0,2 s (8739, 6545). O defeito está no servidor localizar o bitstream, não no endereçamento.

Estrutura das sequências nos itens do IMS: 1 = preservação, 2 = thumbnail (sem bitstream de licença, ao contrário dos do Museu Aeroespacial/Arquivo Nacional/Fiocruz, onde 2 é licença e 3 é thumbnail). Varrer 1–6 nesses itens só gasta tempo: 3–6 não existem.

Recuperado nesta rodada: 8396 (Biblioteca Nacional, sequência 3, 13,5 MB, 3361×4453) — mesmo padrão de 6656 e 7939, onde o catálogo aponta a sequência 2 (licença) e a imagem está na 3.

Achado 2.28 — a cópia legada: só existe para a Biblioteca Nacional, e 25 "cópias" são páginas HTML

Medido em 14/09. A cópia legada vem do objdigital.bn.br e só existe para itens da Biblioteca Nacional: 2.702 dos 13.060 têm legacy_url, todos da BN. Os 16% de cobertura não são falta — os outros 10.358 itens não têm cópia legada na origem.

elegíveis (legacy_url no catálogo) 2.702
no bucket como legado/ 2.137
— imagens JPG 1.899 (859 MB)
— PDF de álbum 213 (8,3 GB)
— páginas HTML 25 (~2,4 KB cada)
HTTP 404 permanente 565 (499 jpg, 41 html, 22 pdf, 2 htm, 1 sid)

As 25 páginas HTML não são imagem. São o visualizador Seadragon do objdigital (viewer.openDzi("GeneratedImages/dzc_output.xml")): a imagem mora numa pirâmide de tiles Deep Zoom ao lado da página. Itens: 586, 587, 630, 802–806, 808, 809, 811, 812, 814, 816, 817, 870, 1080, 1083, 1085, 1086, 3198, 3545, 3546, 3866, 6655. Todos têm imagem de preservação no bucket, então nenhum ficou sem imagem. Não foram apagados — é decisão do usuário (ver pendência). Mesma família do bug de 1081/1082/1084 (HTML de visualizador subido como legado), que foi limpo na época; estes 25 escaparam.

O objdigital não saiu do ar — mudou de endereço. Em 14/09 a porta 80 fechou nos dois IPs (200.9.175.46, 177.223.196.160), mas a 443 está aberta e o site responde em https://objdigital.bn.br/objdigital2/acervo_digital/... (o próprio visualizador Seadragon carrega o script desse prefixo). Prova de que é a mesma fonte: 3 cópias legadas sorteadas do bucket, baixadas pelo endereço novo, voltaram com sha256 idêntico. Correção de uma afirmação anterior desta sessão: o icon573843.jpg usado como "controle" não tinha baixado antes — ele é um dos 565 erros (item 10), e responde 200 no endereço novo.

Consequência: a maior parte dos 565 "404 permanentes" existe no endereço novo. Sondagem completa por HEAD em 14/09 (pipeline/legado-caminho-novo.tsv):

no endereço novo itens
200 com image/jpeg 330
200 com application/pdf 18 (o maior, 52 MB)
200 com text/html (visualizador Deep Zoom, não arquivo) 41
404 de verdade 176

Recuperáveis como arquivo: 348 (~0,3 GB). As 41 páginas HTML somam-se às 25 já no bucket: 66 itens cuja imagem só existe como pirâmide de tiles. A pirâmide Deep Zoom das 25 páginas HTML também responde (item 3545: dzc_output.xml, 5744×4245, tiles de 256 px). **O baixador.py usa o endereço antigo (http://…/acervo_digital/) e precisa ser atualizado antes de qualquer nova passada.

Eu tinha escrito aqui que o gargalo era o link de casa. Errado. Medido em 12/09 (networkQuality): 69 Mbps de upload (~31 GB/h), contra ~1,3 GB/h reais. O subidor.py sobe um arquivo por vez, espera 1,5 s de "estabilidade" e dispara três aws s3 cp por arquivo (arquivo, metadata.json, manifest), cada um pagando a partida do aws-cli: ~5 s por arquivo, seja miniatura de 74 KB ou preservação de 40 MB.

Além de lento, ele estava estragando duas coisas:

  • 873 manifests com linha errada, derivado-thumb256/<handle>.webp. É consequência de eu ter registrado os derivados no ledger: ele lista toda linha do handle, e esse caminho não existe em itens/<handle>/.
  • Duplicatas (N) voltando ao bucket depois da limpeza do achado 2.20 (ex.: 5304, thumb_2.jpg e thumb_2 (1).jpg com o mesmo sha256).

Substituto: pipeline/subidor-paralelo.py (boto3, threads, HEAD antes de apagar o local, manifest uma vez por item, sem linha de derivado, duplicata e rejeito registrados) + pipeline/rodar-paralelo.sh. Teste com boto3 contra o bucket: PUT de 20 MB a 27 Mbps num único stream.

Testes antes de soltar, com o que cada um mostrou:

  1. Lote de 30: todos eram rebaixa já presente no bucket e foram apagados do disco sem HEAD e sem registro. Foi o que motivou a conferência por HEAD e a tabela descartados. Para esses 30, a garantia é só o ledger, reconciliado com o bucket às 12:50.
  2. Lote de 150: 150 descartes, 150 conferidos no bucket.
  3. Lote de 800: 319 enviados (319 conferidos, image/jpeg) + 481 descartes conferidos; manifests dos 319 sem linha de derivado, sha do metadata batendo.
  4. Primeira execução real (18:4x UTC) interrompida: o log mostrava TIFF indo para rejeitados/ (achado 2.19, correção). Assinaturas corrigidas; os 21 da quarentena (19 TIFF + 2 BMP) voltaram para a fila; relançado às 18:59.

Antes do relançamento, 180 arquivos locais (0,25 GB) já presentes no S3 foram apagados do disco a pedido, cada um com MD5 local = ETag do objeto e registro em descartados. Os outros 13.922 não estavam no bucket.

Achado 2.23 — faltam 211 preservações depois da fila, não 73

Cruzando catálogo × (ledger ∪ fila) em 12/09: 12.849 / 13.060 itens terão imagem de preservação quando a fila acabar (thumbnail: 13.059). Os 211:

  • 71 da lista conhecida (faltantes-preservacao.jsonl, recusa da origem);
  • 1 sem preservation_url no catálogo;
  • 139 fora de qualquer lista. Exemplo: 10039–10049, que no bucket têm só thumbnail/ e metadata.json. Desses: 8 rejeitados no upload antigo, 12 com só legado/, ~119 sem rastro (nem bucket, nem fila, nem log).

Hipóteses, nenhuma confirmada: (a) o "12.986/13.059" do achado 2.16 contou objetos legado/ e manifest como preservação, porque o awk só excluía metadata.json e thumbnail; (b) parte dos ~119 pode ser TIFF apagado pelo subidor.py antigo antes do trecho que o subidor.log cobre.

Achado 2.21 — a sessão de 11/09 já tinha as miniaturas no finland, e eu não olhei

/root/brasiliana-wall/ no finland tinha, desde 11/09 13:17–13:41: downloads-flat/ (20.411 arquivos, todos THUMB__, 2,2 GB), s3-thumbs/ (5.168 handles do bucket, 851 MB) e o web/atlas/ cujos quatro .webp batem byte a byte com o que estava publicado no xyz até 12/09. O mural de 12.552 peças foi construído lá, a partir das miniaturas do DSpace — não de ~/Downloads, como o comentário do build-atlas.js leva a crer.

A união desses dois diretórios cobre 13.041 handles, praticamente todos JPEG válido. Eu puxei ~63 GB de imagens de preservação do bucket para derivar 12.985 miniaturas sem ver que 3 GB de miniatura já estavam no disco da mesma máquina, cobrindo mais itens que o meu resultado. Aproveitados depois: +66 peças (74 no delta, 8 eram licença).

Lição de runbook: antes de gerar qualquer coisa no finland, ls /root e cat /root/.bash_history. Sessões anteriores deixam material lá e nada no projeto local registra isso.

Limpeza feita em 12/09: /root/brasiliana-wall e /root/thumbs apagados (3,4 GB; disco 27 → 31 GB livres), depois de verificar que os 13.041 handles estavam todos cobertos pelo bucket (7.038) ou pela fila do Mac (6.785) — zero cópias exclusivas. Ficaram de pé, de propósito: /root/.s3env (600, de 08/09, anterior às nossas sessões — pode ser setup permanente da máquina) e /root/brasiliana/ (44 MB, baixador.py + catálogo + sqlite com 13 linhas).

Achado 2.19 — a contaminação de LICENÇA também atinge a preservação, não só a miniatura

O achado 2.8b tratou isso como problema da trilha de thumbnail. Não é: os handles 20.500.12156.1/6656 e 20.500.12156.1/7939 têm, em ~/Downloads, um "imagem.jpg" que é na verdade o bitstream de LICENÇA do DSpace — 777 bytes de texto UTF-8 com CRLF e extensão .jpg. A validação de magic bytes do subidor.py barra no upload, então nada disso chegou ao bucket; o efeito é que esses dois itens não têm imagem coletada e ninguém percebeu, porque a fila local os contava como prontos.

Implicação: qualquer contagem de "preservação pronta" que leia ~/Downloads pelo nome do arquivo está 2 itens otimista. A contagem pelo bucket está certa.

Atualização 12/09: não são 2, são 10. Ao derivar as miniaturas do material do finland (achado 2.21) apareceram mais oito com o mesmo defeito, todos na trilha de thumbnail: 7802, 7814, 7817, 7848, 7852, 7861, 7875, 8075. São exatamente os itens que sobram fora do mural. O bloco 78xx sugere que a contaminação vem em lote, por faixa de handle — vale varrer essa faixa inteira antes de dar o acervo por fechado.

Correção 12/09 (noite) — a regra de validação estava errada. O subidor.py só aceitava JPEG/PNG e apagava o resto como "bitstream de licença". Pelo próprio subidor.log, dos 40 arquivos que ele apagou assim, só 1 era texto de licença: 36 eram TIFF (II*\0, ~8 MB, com extensão .jpg), 2 BMP e 1 ZIP (001AAN014011.jpg.zip, item 2384). Resultado, item a item (lista com URL em pipeline/apagados-pelo-subidor-antigo.jsonl):

  • 8 itens ficaram sem nenhuma imagem de preservação: 14358, 14359, 14360, 14361, 14362, 14363, 14364, 14368 (TIFF, série "Alb 0261").
  • 18 itens perderam só bitstreams extras (extra_seqN, TIFF), mas têm a imagem principal no bucket ou na fila. 10993/10995 (BMP) e 2384 (ZIP) voltaram por rebaixa e estão na fila. O subidor-paralelo.py aceita TIFF/BMP/GIF/JP2/WebP pela assinatura e manda qualquer outra coisa para pipeline/rejeitados/ com linha na tabela rejeitados — nunca apaga. Os 8 casos 78xx de thumbnail com texto de licença (acima) foram detectados pelo vipsthumbnail, não por essa regra, e continuam valendo.

Achado 2.20 — 3.511 duplicatas (N) no bucket, do Chrome rebaixando por cima

Objetos arquivo (1).jpg convivendo com arquivo.jpg byte a byte idêntico — sufixo que o Chrome põe ao rebaixar um arquivo que já existe em ~/Downloads, e que o subidor.py subiu como se fosse nome legítimo. Confirmadas idênticas por ETag + tamanho (não só tamanho) antes de apagar: 3.511 objetos, 0,39 GB, removidos do bucket e do ledger na mesma passada.

Não foram tocados, e precisam de decisão:

  • 30 objetos cujo gêmeo tem tamanho diferente — não são duplicata, são versões distintas do mesmo nome. Precisam de conferência individual.
  • 43 objetos cujo nome tem (N) mas que são o único arquivo do item — candidatos a renomear, nunca a apagar.

aws s3api list-objects-v2 com --query "Contents[].[Key,Size,ETag]" resolve isso sem baixar nada; o ETag é o MD5 pra upload simples (só os 2.612 objetos multipart do bucket não servem pra comparação direta).

Achado 3.3 — o thumbnail/ do DSpace não é uma miniatura

Medido em amostra de 300: 721 a 3.709 px no maior lado, média 1.087; menor lado nunca abaixo de 462. São derivados de resolução média, não miniaturas de listagem. Duas consequências:

  1. Servem tão bem quanto a imagem de preservação pro tile de 128px do mural — por isso deu pra cobrir os 164 itens sem preservação sem perda visível.
  2. A linha "Miniatura | ~13.005 elegíveis" do MANIFEST descreve ~1 MB por arquivo, não ~8 KB. Quem ler esperando miniatura vai baixar 100x mais do que imagina.

Achado 3.4 — publicar o mural agora exige mandar o atlas/ junto

O wall.html de 12:32 passou a derivar handle (chave do map) e source_url (mesmo prefixo pra todo item) em JS, em vez de lê-los de cada peça. O summarize() do build-atlas.js acompanhou. Resultado: 8,2 MB de map com 12.983 peças contra 10 MB com 12.552 — menos peso com 431 peças a mais.

Mas isso quebra a nota antiga de que "o atlas/ já está commitado lá e não muda a cada alteração do mural": map e wall.html agora são um par acoplado por formato. Publicar um sem o outro deixa o mural sem link e sem busca por handle.

Pendente

  • dc.rights ausente em 5.833 itens (44,7%) — confirmado como característica real da fonte, não bug. Só precisa ficar documentado no MANIFEST final, sem ação de reparo.

  • web/build-atlas.js não está versionado em lugar nenhum — brasiliano/ não é repo git e o xyz só recebe o index.html e o atlas/. A correção do summarize()/pickAll() (achado 3.1) vive só no disco desta máquina; um rm leva junto. Mesma exposição vale para pipeline/ inteiro.

  • Encolher mais o atlas-map.json só com dicionário (achado 3.2): 8,00 → 5,14 MB crus por ~120 KB no fio. Só vale se o JSON.parse no celular virar problema medido — não antes.

  • 30 objetos (N) com tamanho diferente do gêmeo (achado 2.20) — conferência individual, não dá pra automatizar com segurança.

  • 43 objetos (N) que são o único arquivo do item (achado 2.20) — renomear tirando o sufixo, pra o nome não mentir sobre haver um original ao lado.

  • Handles 6656 e 7939 sem imagem de preservação (achado 2.19) — rebaixar; o que há hoje na fila local é o bitstream de licença.

  • Conferir os códigos HTTP que nunca foram tabulados — o runbook do achado 2.18 manda rodar FALTA.erros() no console e rotear por código (404 → reparar URL, 403 → bloqueio de origem, 500 → transitório, retomar). Esse passo nunca foi executado: o achado 2.18 concluiu "é recusa da origem" a partir do status() agregado, não da tabela de erros por handle. Sem isso não dá pra saber quantos dos 71 são reparáveis.

  • Reconferir o handle 7523 (HTTP 500) — registrado uma vez e tratado como falha permanente da origem desde então, mas 500 costuma ser transitório e nunca houve segunda tentativa. Não dá pra testar por curl, e agora está provado com controle (12/09): com User-Agent, Accept, Accept-Language e Referer de Chrome, o 7523 devolve 403 de 5.484 bytes (página do Cloudflare) — e o item 10, que sabidamente funciona e está no bucket, devolve exatamente o mesmo 403 de 5.484 bytes. É bloqueio geral a requisição sem sessão real, nada específico do 7523. Precisa da aba; a extensão Claude-in-Chrome seguia desconectada em duas tentativas no mesmo dia.

  • Reconciliar 13.058 vs 13.060 — o MANIFEST fala em 13.058 itens reais, o catalogo.jsonl tem 13.060 handles distintos. Diferença de 2 nunca explicada; anotada no MANIFEST como pendência.

  • Depois que os 212 faltantes baixarem e subirem (pedido do usuário, 13/09): 1. conferir se todos os itens têm miniatura em derivados/thumb256/ e gerar as que faltarem (as novas preservações não têm derivado ainda); 2. regerar o mural 3D com o conjunto completo — de preferência derivando no finland direto do bucket (itens/*/preservacao/*), que é tráfego intra-Hetzner, em vez de puxar pro Mac; 3. republicar wall.html + atlas/ no rafapolo/xyz (par acoplado, achado 3.4).

  • Quando o rodar-paralelo.sh terminar (log termina em ===== ... FIM): 1. conferir no log as três fases sem !!!, a linha FIM da fila (enviados, duplicatas, rejeitados, falhas) e os pendente: que ficaram no disco; 2. ls pipeline/rejeitados/ + select * from rejeitados — só deve ter texto de licença; imagem ali é bug de assinatura; 3. reconciliar ledger × bucket (mesmo diff do achado de 12/09: 0 divergências nos dois sentidos) e amostrar manifests (grep derivado = 0); 4. varrer ~/Downloads: apagar o que tiver MD5 = ETag no bucket (mesmo método dos 180 de 12/09), manter o resto; 5. atualizar a tabela de contagens do MANIFEST com os números fechados.

  • Rebaixar o que o subidor.py antigo apagou (achado 2.19, correção): 8 itens sem preservação (14358–14364, 14368) + ~20 extra_seqN em TIFF — lista com URL em pipeline/apagados-pelo-subidor-antigo.jsonl. Precisa da aba do navegador.

  • 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)

  • 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).