From 6e54b65bb373b766aee518240320535afbf277c3 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Thu, 30 Jul 2026 18:25:17 -0300 Subject: [PATCH 01/36] feat(web): indicador visual de carregamento ao validar pacote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - botao "Validar" ganha um spinner (span .spinner) e o texto muda para "Validando..." assim que o formulario e submetido - botao fica desabilitado durante o envio, evitando duplo clique - listener em "pageshow" reseta o botao ao estado normal caso o usuario volte para a pagina pelo cache do navegador (bfcache), ja que nesse caso o JS nao roda de novo do zero - testado com Playwright (headless): estado normal, estado "Validando..." (via evento submit) e reset apos voltar, alem do fluxo real de upload+clique+redirect ponta a ponta Porque: a validacao de pacotes maiores demora e, sem nenhum feedback visual, a tela parece travada — primeiro item da issue #36 (melhorias de UI/UX). A traducao da nova string "Validando..." fica para um commit posterior, junto com as demais strings novas da issue #36, seguindo o mesmo padrao usado na issue #34. Co-Authored-By: Claude Sonnet 5 --- .../src/spsvalidator/web/templates/index.html | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/spsvalidator/src/spsvalidator/web/templates/index.html b/spsvalidator/src/spsvalidator/web/templates/index.html index 9d07eea..1b6cb90 100644 --- a/spsvalidator/src/spsvalidator/web/templates/index.html +++ b/spsvalidator/src/spsvalidator/web/templates/index.html @@ -65,6 +65,32 @@ .download-feedback.error { color: #a30000; } + #validate-button { + display: inline-flex; + align-items: center; + gap: 8px; + } + #validate-button:disabled { + cursor: not-allowed; + opacity: 0.7; + } + #validate-button .spinner { + display: none; + width: 14px; + height: 14px; + border: 2px solid rgba(0, 0, 0, 0.2); + border-top-color: #1a1a1a; + border-radius: 50%; + animation: spin 0.7s linear infinite; + } + #validate-button.is-loading .spinner { + display: inline-block; + } + @keyframes spin { + to { + transform: rotate(360deg); + } + } @@ -84,9 +110,12 @@

{{ app_display_name }}

{{ _("Validar pacote SPS") }}

-
+ - +
{% if error_message %}

{{ error_message }}

@@ -168,6 +197,33 @@

{{ _("Artigos Considerados") }}

\ No newline at end of file From 180d49b449723e6757df2fae9a7c74b86d445901 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 10:25:07 -0300 Subject: [PATCH 20/36] i18n(web): traduz as strings novas do relatorio redesenhado para en/es - Roda pybabel extract/update e traduz para en/es as 9 strings introduzidas pela reconstrucao visual do relatorio HTML: "Historico", "Acoes do relatorio", "Resumo do relatorio", "Total de ocorrencias", "Filtrar por gravidade", "Todos (%(count)s)", "Buscar no relatorio", "Buscar problema, acao ou detalhe" e "Nenhum resultado para os filtros atuais.". - O merge de main trouxe tambem strings do PR #35 (ex.: "Relatorio", "Pacote", "Gravidade") que o pybabel update tentou casar por similaridade (fuzzy matching) com strings ja existentes - duas ficaram erradas ("Validando..." virou "Validate", "Baixar CSV de %(name)s" virou "Download CSV of %(name)s") e foram corrigidas aqui para o texto correto ja usado antes do merge. Nao mexido (fora do escopo): duas strings ja tinham traducao errada em en *antes* deste merge, herdada do main/PR #35 - "Valido" e "Limpar" ambas com msgstr "Validate" - registrado para uma issue de i18n futura, nao corrigido aqui pra nao misturar com o trabalho desta branch. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JMzp8hGLSHNcMaNPCZSf5h --- .../translations/en/LC_MESSAGES/messages.po | 90 +++++++++++++------ .../translations/es/LC_MESSAGES/messages.po | 90 +++++++++++++------ .../translations/pt/LC_MESSAGES/messages.po | 85 ++++++++++++------ 3 files changed, 182 insertions(+), 83 deletions(-) diff --git a/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po b/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po index e132c7c..431d143 100644 --- a/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po +++ b/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: spsvalidator 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-31 09:25-0300\n" +"POT-Creation-Date: 2026-07-31 09:39-0300\n" "PO-Revision-Date: 2026-07-14 10:59-0300\n" "Last-Translator: SciELO\n" "Language: en\n" @@ -55,13 +55,13 @@ msgid "Categoria" msgstr "Category" #: src/spsvalidator/web/routes.py:241 -#: src/spsvalidator/web/templates/report.html:194 +#: src/spsvalidator/web/templates/report.html:826 msgid "Problema" msgstr "Issue" #: src/spsvalidator/web/routes.py:242 -#: src/spsvalidator/web/templates/report.html:173 -#: src/spsvalidator/web/templates/report.html:195 +#: src/spsvalidator/web/templates/report.html:798 +#: src/spsvalidator/web/templates/report.html:827 msgid "Ação de correção" msgstr "Corrective action" @@ -130,7 +130,6 @@ msgid "Baixar CSV de %(name)s" msgstr "Download CSV for %(name)s" #: src/spsvalidator/web/templates/_history_list.html:53 -#: src/spsvalidator/web/templates/report.html:135 msgid "Baixar CSV" msgstr "Download CSV" @@ -236,53 +235,83 @@ msgstr "Failed to download CSV." msgid "Validando..." msgstr "Validating..." -#: src/spsvalidator/web/templates/report.html:123 +#: src/spsvalidator/web/templates/report.html:663 msgid "Relatório de validação agrupado por gravidade e categoria" msgstr "Validation report grouped by severity and category" -#: src/spsvalidator/web/templates/report.html:126 -#, python-format -msgid "%(total)s ocorrências no total" -msgstr "%(total)s occurrences in total" +#: src/spsvalidator/web/templates/report.html:667 +msgid "Ações do relatório" +msgstr "Report actions" -#: src/spsvalidator/web/templates/report.html:127 -#: src/spsvalidator/web/templates/report.html:147 -#: src/spsvalidator/web/templates/report.html:153 -msgid "corrigidas" -msgstr "" +#: src/spsvalidator/web/templates/report.html:670 +msgid "Histórico" +msgstr "History" -#: src/spsvalidator/web/templates/report.html:129 +#: src/spsvalidator/web/templates/report.html:678 msgid "Limpar marcações" msgstr "Clear markings" -#: src/spsvalidator/web/templates/report.html:134 -msgid "Voltar ao histórico" -msgstr "Back to history" +#: src/spsvalidator/web/templates/report.html:687 +msgid "Resumo do relatório" +msgstr "Report summary" + +#: src/spsvalidator/web/templates/report.html:690 +msgid "Total de ocorrências" +msgstr "Total occurrences" + +#: src/spsvalidator/web/templates/report.html:700 +msgid "Corrigidas" +msgstr "Fixed" + +#: src/spsvalidator/web/templates/report.html:719 +msgid "Filtrar por gravidade" +msgstr "Filter by severity" + +#: src/spsvalidator/web/templates/report.html:721 +#, python-format +msgid "Todos (%(count)s)" +msgstr "All (%(count)s)" + +#: src/spsvalidator/web/templates/report.html:731 +msgid "Buscar no relatório" +msgstr "Search in report" -#: src/spsvalidator/web/templates/report.html:140 +#: src/spsvalidator/web/templates/report.html:735 +msgid "Buscar problema, ação ou detalhe" +msgstr "Search problem, action or detail" + +#: src/spsvalidator/web/templates/report.html:741 msgid "Nenhuma ocorrência encontrada para este pacote." msgstr "No occurrences found for this package." -#: src/spsvalidator/web/templates/report.html:158 +#: src/spsvalidator/web/templates/report.html:754 +msgid "corrigidas" +msgstr "fixed" + +#: src/spsvalidator/web/templates/report.html:779 #, python-format msgid "%(count)s ocorrências" msgstr "%(count)s occurrences" -#: src/spsvalidator/web/templates/report.html:165 +#: src/spsvalidator/web/templates/report.html:788 msgid "Marcar todas como corrigidas" msgstr "Mark all as fixed" -#: src/spsvalidator/web/templates/report.html:171 -#: src/spsvalidator/web/templates/report.html:192 +#: src/spsvalidator/web/templates/report.html:801 +#: src/spsvalidator/web/templates/report.html:830 +msgid "Detalhes técnicos" +msgstr "Technical details" + +#: src/spsvalidator/web/templates/report.html:813 +#: src/spsvalidator/web/templates/report.html:842 msgid "Corrigido" msgstr "Fixed" -#: src/spsvalidator/web/templates/report.html:176 -#: src/spsvalidator/web/templates/report.html:198 -msgid "Detalhes técnicos" -msgstr "Technical details" +#: src/spsvalidator/web/templates/report.html:855 +msgid "Nenhum resultado para os filtros atuais." +msgstr "No results for the current filters." -#: src/spsvalidator/web/templates/report.html:307 +#: src/spsvalidator/web/templates/report.html:963 msgid "Limpar todas as marcações deste relatório?" msgstr "Clear all markings in this report?" @@ -292,3 +321,6 @@ msgstr "Clear all markings in this report?" #~ msgid "PDF" #~ msgstr "" +#~ msgid "%(total)s ocorrências no total" +#~ msgstr "%(total)s occurrences in total" + diff --git a/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po b/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po index 9a29723..d71bd55 100644 --- a/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po +++ b/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: spsvalidator 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-31 09:25-0300\n" +"POT-Creation-Date: 2026-07-31 09:39-0300\n" "PO-Revision-Date: 2026-07-14 10:59-0300\n" "Last-Translator: SciELO\n" "Language: es\n" @@ -55,13 +55,13 @@ msgid "Categoria" msgstr "Categoría" #: src/spsvalidator/web/routes.py:241 -#: src/spsvalidator/web/templates/report.html:194 +#: src/spsvalidator/web/templates/report.html:826 msgid "Problema" msgstr "Problema" #: src/spsvalidator/web/routes.py:242 -#: src/spsvalidator/web/templates/report.html:173 -#: src/spsvalidator/web/templates/report.html:195 +#: src/spsvalidator/web/templates/report.html:798 +#: src/spsvalidator/web/templates/report.html:827 msgid "Ação de correção" msgstr "Acción de corrección" @@ -129,7 +129,6 @@ msgid "Baixar CSV de %(name)s" msgstr "Descargar CSV de %(name)s" #: src/spsvalidator/web/templates/_history_list.html:53 -#: src/spsvalidator/web/templates/report.html:135 msgid "Baixar CSV" msgstr "Descargar CSV" @@ -234,53 +233,83 @@ msgstr "Error al descargar CSV." msgid "Validando..." msgstr "Validando..." -#: src/spsvalidator/web/templates/report.html:123 +#: src/spsvalidator/web/templates/report.html:663 msgid "Relatório de validação agrupado por gravidade e categoria" msgstr "Informe de validación agrupado por gravedad y categoría" -#: src/spsvalidator/web/templates/report.html:126 -#, python-format -msgid "%(total)s ocorrências no total" -msgstr "%(total)s ocurrencias en total" +#: src/spsvalidator/web/templates/report.html:667 +msgid "Ações do relatório" +msgstr "Acciones del informe" -#: src/spsvalidator/web/templates/report.html:127 -#: src/spsvalidator/web/templates/report.html:147 -#: src/spsvalidator/web/templates/report.html:153 -msgid "corrigidas" -msgstr "" +#: src/spsvalidator/web/templates/report.html:670 +msgid "Histórico" +msgstr "Historial" -#: src/spsvalidator/web/templates/report.html:129 +#: src/spsvalidator/web/templates/report.html:678 msgid "Limpar marcações" msgstr "Borrar marcas" -#: src/spsvalidator/web/templates/report.html:134 -msgid "Voltar ao histórico" -msgstr "Volver al historial" +#: src/spsvalidator/web/templates/report.html:687 +msgid "Resumo do relatório" +msgstr "Resumen del informe" + +#: src/spsvalidator/web/templates/report.html:690 +msgid "Total de ocorrências" +msgstr "Total de ocurrencias" + +#: src/spsvalidator/web/templates/report.html:700 +msgid "Corrigidas" +msgstr "Corregidas" + +#: src/spsvalidator/web/templates/report.html:719 +msgid "Filtrar por gravidade" +msgstr "Filtrar por gravedad" + +#: src/spsvalidator/web/templates/report.html:721 +#, python-format +msgid "Todos (%(count)s)" +msgstr "Todos (%(count)s)" + +#: src/spsvalidator/web/templates/report.html:731 +msgid "Buscar no relatório" +msgstr "Buscar en el informe" -#: src/spsvalidator/web/templates/report.html:140 +#: src/spsvalidator/web/templates/report.html:735 +msgid "Buscar problema, ação ou detalhe" +msgstr "Buscar problema, acción o detalle" + +#: src/spsvalidator/web/templates/report.html:741 msgid "Nenhuma ocorrência encontrada para este pacote." msgstr "No se encontraron ocurrencias para este paquete." -#: src/spsvalidator/web/templates/report.html:158 +#: src/spsvalidator/web/templates/report.html:754 +msgid "corrigidas" +msgstr "corregidas" + +#: src/spsvalidator/web/templates/report.html:779 #, python-format msgid "%(count)s ocorrências" msgstr "%(count)s ocurrencias" -#: src/spsvalidator/web/templates/report.html:165 +#: src/spsvalidator/web/templates/report.html:788 msgid "Marcar todas como corrigidas" msgstr "Marcar todas como corregidas" -#: src/spsvalidator/web/templates/report.html:171 -#: src/spsvalidator/web/templates/report.html:192 +#: src/spsvalidator/web/templates/report.html:801 +#: src/spsvalidator/web/templates/report.html:830 +msgid "Detalhes técnicos" +msgstr "Detalles técnicos" + +#: src/spsvalidator/web/templates/report.html:813 +#: src/spsvalidator/web/templates/report.html:842 msgid "Corrigido" msgstr "Corregido" -#: src/spsvalidator/web/templates/report.html:176 -#: src/spsvalidator/web/templates/report.html:198 -msgid "Detalhes técnicos" -msgstr "Detalles técnicos" +#: src/spsvalidator/web/templates/report.html:855 +msgid "Nenhum resultado para os filtros atuais." +msgstr "No se encontraron resultados para los filtros actuales." -#: src/spsvalidator/web/templates/report.html:307 +#: src/spsvalidator/web/templates/report.html:963 msgid "Limpar todas as marcações deste relatório?" msgstr "¿Borrar todas las marcas de este informe?" @@ -290,3 +319,6 @@ msgstr "¿Borrar todas las marcas de este informe?" #~ msgid "PDF" #~ msgstr "" +#~ msgid "%(total)s ocorrências no total" +#~ msgstr "%(total)s ocurrencias en total" + diff --git a/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po b/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po index 0d633d2..99fd225 100644 --- a/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po +++ b/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: spsvalidator 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-31 09:25-0300\n" +"POT-Creation-Date: 2026-07-31 09:39-0300\n" "PO-Revision-Date: 2026-07-14 10:59-0300\n" "Last-Translator: SciELO\n" "Language: pt\n" @@ -55,13 +55,13 @@ msgid "Categoria" msgstr "" #: src/spsvalidator/web/routes.py:241 -#: src/spsvalidator/web/templates/report.html:194 +#: src/spsvalidator/web/templates/report.html:826 msgid "Problema" msgstr "" #: src/spsvalidator/web/routes.py:242 -#: src/spsvalidator/web/templates/report.html:173 -#: src/spsvalidator/web/templates/report.html:195 +#: src/spsvalidator/web/templates/report.html:798 +#: src/spsvalidator/web/templates/report.html:827 msgid "Ação de correção" msgstr "" @@ -129,7 +129,6 @@ msgid "Baixar CSV de %(name)s" msgstr "" #: src/spsvalidator/web/templates/_history_list.html:53 -#: src/spsvalidator/web/templates/report.html:135 msgid "Baixar CSV" msgstr "" @@ -234,53 +233,83 @@ msgstr "" msgid "Validando..." msgstr "" -#: src/spsvalidator/web/templates/report.html:123 +#: src/spsvalidator/web/templates/report.html:663 msgid "Relatório de validação agrupado por gravidade e categoria" msgstr "" -#: src/spsvalidator/web/templates/report.html:126 -#, python-format -msgid "%(total)s ocorrências no total" +#: src/spsvalidator/web/templates/report.html:667 +msgid "Ações do relatório" msgstr "" -#: src/spsvalidator/web/templates/report.html:127 -#: src/spsvalidator/web/templates/report.html:147 -#: src/spsvalidator/web/templates/report.html:153 -msgid "corrigidas" +#: src/spsvalidator/web/templates/report.html:670 +msgid "Histórico" msgstr "" -#: src/spsvalidator/web/templates/report.html:129 +#: src/spsvalidator/web/templates/report.html:678 msgid "Limpar marcações" msgstr "" -#: src/spsvalidator/web/templates/report.html:134 -msgid "Voltar ao histórico" +#: src/spsvalidator/web/templates/report.html:687 +msgid "Resumo do relatório" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:690 +msgid "Total de ocorrências" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:700 +msgid "Corrigidas" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:719 +msgid "Filtrar por gravidade" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:721 +#, python-format +msgid "Todos (%(count)s)" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:731 +msgid "Buscar no relatório" msgstr "" -#: src/spsvalidator/web/templates/report.html:140 +#: src/spsvalidator/web/templates/report.html:735 +msgid "Buscar problema, ação ou detalhe" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:741 msgid "Nenhuma ocorrência encontrada para este pacote." msgstr "" -#: src/spsvalidator/web/templates/report.html:158 +#: src/spsvalidator/web/templates/report.html:754 +msgid "corrigidas" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:779 #, python-format msgid "%(count)s ocorrências" msgstr "" -#: src/spsvalidator/web/templates/report.html:165 +#: src/spsvalidator/web/templates/report.html:788 msgid "Marcar todas como corrigidas" msgstr "" -#: src/spsvalidator/web/templates/report.html:171 -#: src/spsvalidator/web/templates/report.html:192 +#: src/spsvalidator/web/templates/report.html:801 +#: src/spsvalidator/web/templates/report.html:830 +msgid "Detalhes técnicos" +msgstr "" + +#: src/spsvalidator/web/templates/report.html:813 +#: src/spsvalidator/web/templates/report.html:842 msgid "Corrigido" msgstr "" -#: src/spsvalidator/web/templates/report.html:176 -#: src/spsvalidator/web/templates/report.html:198 -msgid "Detalhes técnicos" +#: src/spsvalidator/web/templates/report.html:855 +msgid "Nenhum resultado para os filtros atuais." msgstr "" -#: src/spsvalidator/web/templates/report.html:307 +#: src/spsvalidator/web/templates/report.html:963 msgid "Limpar todas as marcações deste relatório?" msgstr "" @@ -290,3 +319,9 @@ msgstr "" #~ msgid "PDF" #~ msgstr "" +#~ msgid "%(total)s ocorrências no total" +#~ msgstr "" + +#~ msgid "Voltar ao histórico" +#~ msgstr "" + From fcc807534df195f5c748a187c0374d3b0bed8849 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:14:29 -0300 Subject: [PATCH 21/36] fix(packaging): gera build_info.py real no build Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona a chamada a `generate_build_info.sh` em `build_linux.sh`, igual ao que `build_macos.sh` já fazia. Porque: `build_info.py` e um arquivo estatico versionado com valores de macOS (BUILD_PLATFORM = "macOS"). Como `build_linux.sh` nunca regenerava esse arquivo antes de empacotar, o binario Linux herdava o conteudo commitado e o rodape da aplicacao continuava exibindo "Compilado para macOS" mesmo rodando em Linux. `generate_build_info.sh` ja tratava corretamente plataformas nao-Darwin (grava BUILD_PLATFORM real via `uname -s` e BUILD_MACOS_VERSION = "development"); so faltava ser chamado nesse script. Causa raiz identificada durante a review do PR #37 (pitangainnovare) e relacionada a issue #32. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- spsvalidator/packaging/build_linux.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/spsvalidator/packaging/build_linux.sh b/spsvalidator/packaging/build_linux.sh index 2b4a38f..808554c 100644 --- a/spsvalidator/packaging/build_linux.sh +++ b/spsvalidator/packaging/build_linux.sh @@ -7,6 +7,7 @@ cd "$ROOT_DIR" python -m pip install -e ".[dev]" python -m pip install pyinstaller pybabel compile -d src/spsvalidator/translations +bash packaging/generate_build_info.sh pyinstaller --noconfirm --windowed \ --name spsvalidator \ --icon src/spsvalidator/web/static/img/icon.png \ From 5cc30284e7521d0a3a24b8d9719ab2d9323143b6 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:14:40 -0300 Subject: [PATCH 22/36] fix(web): corrige rotulo de PDF por prefixo e formato de data no historico MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `_short_pdf_label()`: so encurta o nome do PDF quando o caractere logo apos o `xml_stem` for "-" ou "_"; caso contrario mantem o nome completo do arquivo. - `_format_validated_at()`: troca `isoformat(timespec="seconds")` por `strftime("%Y-%m-%d %H:%M:%S")`. Porque: `_short_pdf_label()` tinha um bug de fronteira de string — `stem.startswith(xml_stem)` e verdadeiro pra qualquer nome de arquivo que comece com o stem, mesmo sem separador. Com `xml_stem="art"` e `filename="article.pdf"`, o corte gerava o rotulo errado "icle" em vez do nome completo. `_format_validated_at()` ainda expunha o "T" do ISO 8601 e o offset "+00:00" (o timespec="seconds" so removia os microssegundos), o que e ruido pra leitura na tabela de historico; o valor gravado continua em UTC, so sem indicar o timezone explicitamente. Ambos os pontos foram levantados na review do PR #37 (pitangainnovare). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- spsvalidator/src/spsvalidator/web/routes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spsvalidator/src/spsvalidator/web/routes.py b/spsvalidator/src/spsvalidator/web/routes.py index 5a02a20..7bde58a 100644 --- a/spsvalidator/src/spsvalidator/web/routes.py +++ b/spsvalidator/src/spsvalidator/web/routes.py @@ -82,7 +82,7 @@ def _short_pdf_label(xml_stem: str, filename: str) -> str: stem = filename[:-4] if filename.lower().endswith(".pdf") else filename if stem == xml_stem: return gettext("PDF principal") - if stem.startswith(xml_stem): + if stem.startswith(xml_stem) and stem[len(xml_stem):len(xml_stem) + 1] in ("-", "_"): suffix = stem[len(xml_stem):].lstrip("-_") if suffix: return suffix @@ -114,13 +114,14 @@ def _pdf_previews_by_article(package_sha256: str) -> list[dict]: def _format_validated_at(value: str) -> str: - """Trunca os microssegundos de "validated_at" pra exibicao na tabela. + """Formata "validated_at" como "AAAA-MM-DD HH:MM:SS" pra exibicao na tabela. O valor e gravado com datetime.now(UTC).isoformat(), que inclui - microssegundos; isso e ruido pra quem esta lendo a lista de historico. + microssegundos e o offset "+00:00"; ambos sao ruido pra quem esta + lendo a lista de historico. """ try: - return datetime.fromisoformat(value).isoformat(timespec="seconds") + return datetime.fromisoformat(value).strftime("%Y-%m-%d %H:%M:%S") except ValueError: return value From 21d58167c6c3ededdd0ac322a8bcec2e9b8044d2 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:14:48 -0300 Subject: [PATCH 23/36] i18n(web): usa msgid em pt-BR pra coluna de excecoes do historico MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Troca `_("Exceptions")` por `_("Exceções")` no cabecalho da tabela de historico. Porque: a review do PR #37 (pitangainnovare) apontou que o termo aparecia sem traducao tanto no catalogo pt quanto no es. A causa nao era falta de traducao em es — era o msgid de origem estar em ingles por engano, diferente das colunas vizinhas ("Ações", "Relatório"), que ja nascem em pt-BR. Alinhar o msgid ao idioma-fonte do app resolve os dois comentarios de uma vez: pt continua com msgstr vazio (convencao ja usada no resto do catalogo) e en/es passam a ter uma traducao explicita de verdade. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- spsvalidator/src/spsvalidator/web/templates/_history_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spsvalidator/src/spsvalidator/web/templates/_history_list.html b/spsvalidator/src/spsvalidator/web/templates/_history_list.html index e3c240c..ac0b6ff 100644 --- a/spsvalidator/src/spsvalidator/web/templates/_history_list.html +++ b/spsvalidator/src/spsvalidator/web/templates/_history_list.html @@ -11,7 +11,7 @@ {{ _("CRITICAL") }} {{ _("ERROR") }} {{ _("WARNING") }} - {{ _("Exceptions") }} + {{ _("Exceções") }} {{ _("Ações") }} From 4510e7a683f0710b18491eced949754c6a37ab8b Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:14:56 -0300 Subject: [PATCH 24/36] style(web): encurta e reordena as abas de Artigos/Pacotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rotulos das abas: "Artigos Considerados"/"Pacotes validados" viram "Artigos"/"Pacotes". - Ordem visual invertida: "Pacotes" aparece antes de "Artigos". Porque: rotulos mais curtos foram sugeridos na review do PR #37 (pitangainnovare); a ordem foi invertida depois, a pedido do usuario durante a validacao manual da tela. A aba "Artigos" continua selecionada por padrao (`aria-selected="true"`) pra preservar o comportamento do item 9 da issue #36 — o resultado da validacao fica visivel imediatamente apos validar um pacote, sem precisar trocar de aba; so a posicao visual dos botoes mudou. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- spsvalidator/src/spsvalidator/web/templates/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spsvalidator/src/spsvalidator/web/templates/index.html b/spsvalidator/src/spsvalidator/web/templates/index.html index f7cb333..9bd8914 100644 --- a/spsvalidator/src/spsvalidator/web/templates/index.html +++ b/spsvalidator/src/spsvalidator/web/templates/index.html @@ -331,8 +331,8 @@

{{ _("Validar pacote SPS") }}

{% if latest_result %}
- - + +
{% else %}

{{ _("Pacotes validados") }}

From dd6afbd88833029c89916bc285f3fba4dc5aaec8 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:15:04 -0300 Subject: [PATCH 25/36] i18n(web): atualiza catalogo pt pros msgids revisados no PR #37 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - msgid "Exceptions" -> "Exceções" (coluna do historico). - msgid "Artigos Considerados" -> "Artigos" (aba). - Nova entrada msgid "Pacotes" (aba), referenciando index.html:335. Porque: pt e o idioma-fonte do catalogo (msgstr fica vazio em todas as entradas, o proprio msgid ja e o texto exibido), entao esse commit so acompanha as mudancas de msgid feitas em `_history_list.html` e `index.html` nesta mesma leva de correcoes da review do PR #37. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- .../spsvalidator/translations/pt/LC_MESSAGES/messages.po | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po b/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po index 99fd225..fa19444 100644 --- a/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po +++ b/spsvalidator/src/spsvalidator/translations/pt/LC_MESSAGES/messages.po @@ -106,7 +106,7 @@ msgid "WARNING" msgstr "" #: src/spsvalidator/web/templates/_history_list.html:14 -msgid "Exceptions" +msgid "Exceções" msgstr "" #: src/spsvalidator/web/templates/_history_list.html:15 @@ -180,10 +180,13 @@ msgid "Validar" msgstr "" #: src/spsvalidator/web/templates/index.html:334 -msgid "Artigos Considerados" +msgid "Artigos" msgstr "" #: src/spsvalidator/web/templates/index.html:335 +msgid "Pacotes" +msgstr "" + #: src/spsvalidator/web/templates/index.html:338 msgid "Pacotes validados" msgstr "" From 32941ed3d556f2dcff2b9d629d07bb183c5744b1 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:15:11 -0300 Subject: [PATCH 26/36] i18n(web): traduz os msgids revisados no PR #37 pro catalogo en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - msgid "Exceções" -> msgstr "Exceptions". - msgid "Artigos" -> msgstr "Articles" (era "Articles Considered"). - Nova entrada msgid "Pacotes" -> msgstr "Packages". Porque: acompanha a mudanca de msgid feita em `_history_list.html` (coluna de excecoes, que estava sem traducao explicita em ingles) e a mudanca de rotulo das abas em `index.html`, apontadas na review do PR #37 (pitangainnovare). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- .../translations/en/LC_MESSAGES/messages.po | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po b/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po index 431d143..cd15ceb 100644 --- a/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po +++ b/spsvalidator/src/spsvalidator/translations/en/LC_MESSAGES/messages.po @@ -107,8 +107,8 @@ msgid "WARNING" msgstr "" #: src/spsvalidator/web/templates/_history_list.html:14 -msgid "Exceptions" -msgstr "" +msgid "Exceções" +msgstr "Exceptions" #: src/spsvalidator/web/templates/_history_list.html:15 msgid "Ações" @@ -181,10 +181,13 @@ msgid "Validar" msgstr "Validate" #: src/spsvalidator/web/templates/index.html:334 -msgid "Artigos Considerados" -msgstr "Articles Considered" +msgid "Artigos" +msgstr "Articles" #: src/spsvalidator/web/templates/index.html:335 +msgid "Pacotes" +msgstr "Packages" + #: src/spsvalidator/web/templates/index.html:338 msgid "Pacotes validados" msgstr "Validated packages" From 09438245787ec1682fde16f5c1b03102eb267cef Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 31 Jul 2026 12:15:18 -0300 Subject: [PATCH 27/36] i18n(web): traduz os msgids revisados no PR #37 pro catalogo es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - msgid "Exceções" -> msgstr "Excepciones" (antes "Exceptions", sem traducao pro espanhol). - msgid "Artigos" -> msgstr "Artículos" (era "Artículos Considerados"). - Nova entrada msgid "Pacotes" -> msgstr "Paquetes". Porque: resolve diretamente o comentario da review do PR #37 (pitangainnovare) perguntando se "Exceptions" devia ser traduzido no catalogo es, e acompanha a mudanca de rotulo das abas em `index.html`. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YThTF9o31pFcaYTS6MnhLP --- .../translations/es/LC_MESSAGES/messages.po | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po b/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po index d71bd55..024dc68 100644 --- a/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po +++ b/spsvalidator/src/spsvalidator/translations/es/LC_MESSAGES/messages.po @@ -106,8 +106,8 @@ msgid "WARNING" msgstr "" #: src/spsvalidator/web/templates/_history_list.html:14 -msgid "Exceptions" -msgstr "" +msgid "Exceções" +msgstr "Excepciones" #: src/spsvalidator/web/templates/_history_list.html:15 msgid "Ações" @@ -180,10 +180,13 @@ msgid "Validar" msgstr "" #: src/spsvalidator/web/templates/index.html:334 -msgid "Artigos Considerados" -msgstr "Artículos Considerados" +msgid "Artigos" +msgstr "Artículos" #: src/spsvalidator/web/templates/index.html:335 +msgid "Pacotes" +msgstr "Paquetes" + #: src/spsvalidator/web/templates/index.html:338 msgid "Pacotes validados" msgstr "Paquetes validados" From 133b292f0128cbf86bfd985e2c455014eb24ec68 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:57:23 -0300 Subject: [PATCH 28/36] fix(web): adiciona checagem defensiva platform.system() no rodape - Adiciona `platform.system() == "Darwin"` a condicao que decide se mostra "Compilado para macOS", alem das checagens ja existentes (is_running_from_source, BUILD_MACOS_VERSION, BUILD_PLATFORM). Porque: build_info.py e um arquivo estatico que pode ficar desatualizado (ex.: herdado de um build anterior de outra plataforma, como aconteceu com Linux antes do fix fcc8075). Sem essa checagem extra, um build_info.py com BUILD_PLATFORM="macOS" faria o rodape mentir sobre a plataforma real sempre que o script de empacotamento de uma nova plataforma esquecer de regenerar o arquivo - exatamente o padrao de bug ja visto duas vezes. Apontado na 2a rodada de review do PR #37 (pitangainnovare). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- spsvalidator/src/spsvalidator/build_metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/spsvalidator/src/spsvalidator/build_metadata.py b/spsvalidator/src/spsvalidator/build_metadata.py index 7aec9d0..eab3f5f 100644 --- a/spsvalidator/src/spsvalidator/build_metadata.py +++ b/spsvalidator/src/spsvalidator/build_metadata.py @@ -13,6 +13,7 @@ def get_footer_build_label() -> str: not is_running_from_source() and build_info.BUILD_MACOS_VERSION != "development" and build_info.BUILD_PLATFORM == "macOS" + and platform.system() == "Darwin" ): return gettext( "Compilado para macOS %(version)s", From 36406fd05ffe55168a288be099fc4910dbd68397 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:57:38 -0300 Subject: [PATCH 29/36] feat(packaging): cria generate_build_info.ps1 pro build Windows - Porta pro PowerShell a mesma logica de generate_build_info.sh: le a versao do pyproject.toml via tomllib e grava build_info.py com BUILD_PLATFORM = "Windows" e BUILD_MACOS_VERSION = "development". Porque: build_windows.ps1 nunca gerava um build_info.py real, entao o binario Windows empacotava o arquivo estatico versionado no repo (com valores de macOS) e exibia "Compilado para macOS" incorretamente - mesmo bug ja corrigido no Linux (fcc8075), agora tambem no Windows. Apontado na 2a rodada de review do PR #37 (pitangainnovare). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- spsvalidator/packaging/generate_build_info.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 spsvalidator/packaging/generate_build_info.ps1 diff --git a/spsvalidator/packaging/generate_build_info.ps1 b/spsvalidator/packaging/generate_build_info.ps1 new file mode 100644 index 0000000..52b154b --- /dev/null +++ b/spsvalidator/packaging/generate_build_info.ps1 @@ -0,0 +1,14 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$RootDir = Split-Path -Path $PSScriptRoot -Parent +$TargetPath = Join-Path $RootDir "src\spsvalidator\build_info.py" +Set-Location $RootDir + +$AppVersion = python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])" + +@" +APP_VERSION = "$AppVersion" +BUILD_MACOS_VERSION = "development" +BUILD_PLATFORM = "Windows" +"@ | Set-Content -Path $TargetPath -Encoding UTF8 \ No newline at end of file From c43a083c39beaca6800d8cd643f5735560d745cd Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:57:44 -0300 Subject: [PATCH 30/36] fix(packaging): chama generate_build_info.ps1 no build Windows - Adiciona a chamada a generate_build_info.ps1 antes do pyinstaller, no mesmo ponto em que build_linux.sh/build_macos.sh chamam o script equivalente em bash. Porque: sem essa chamada, o build_info.py gerado pelo script anterior nunca chega a ser usado - ver commit anterior pro contexto completo do bug. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- spsvalidator/packaging/build_windows.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/spsvalidator/packaging/build_windows.ps1 b/spsvalidator/packaging/build_windows.ps1 index 19ea96e..7c52171 100644 --- a/spsvalidator/packaging/build_windows.ps1 +++ b/spsvalidator/packaging/build_windows.ps1 @@ -7,6 +7,7 @@ Set-Location $RootDir python -m pip install -e ".[dev]" python -m pip install pyinstaller pybabel compile -d src/spsvalidator/translations +& "$PSScriptRoot\generate_build_info.ps1" pyinstaller --noconfirm --windowed ` --name spsvalidator ` --icon src/spsvalidator/web/static/img/icon.png ` From 24fd5f203bf5cef88d4b020098af83db6bac638e Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:57:53 -0300 Subject: [PATCH 31/36] test(web): cobre get_footer_build_label com build_info desatualizado - 3 testes: rodape correto quando roda em macOS de verdade, rodape ignora build_info.py com BUILD_PLATFORM="macOS" quando platform.system() nao e Darwin, e modo de desenvolvimento. Porque: fixa em teste o comportamento da checagem defensiva adicionada no commit anterior, pra nao regredir se alguem mexer nessa funcao no futuro sem se dar conta do bug que ela previne. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- spsvalidator/tests/test_build_metadata.py | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 spsvalidator/tests/test_build_metadata.py diff --git a/spsvalidator/tests/test_build_metadata.py b/spsvalidator/tests/test_build_metadata.py new file mode 100644 index 0000000..1398bdb --- /dev/null +++ b/spsvalidator/tests/test_build_metadata.py @@ -0,0 +1,32 @@ +from spsvalidator import build_info, build_metadata + + +def test_footer_shows_macos_label_when_built_and_running_on_macos(monkeypatch): + monkeypatch.setattr(build_metadata, "is_running_from_source", lambda: False) + monkeypatch.setattr(build_info, "BUILD_PLATFORM", "macOS") + monkeypatch.setattr(build_info, "BUILD_MACOS_VERSION", "15.0 (24A335)") + monkeypatch.setattr(build_metadata.platform, "system", lambda: "Darwin") + + assert "macOS" in build_metadata.get_footer_build_label() + + +def test_footer_ignores_stale_macos_build_info_on_other_platforms(monkeypatch): + """build_info.py desatualizado (ex.: gerado num build macOS anterior e + reaproveitado sem regenerar) nao deve fazer o rodape mentir sobre a + plataforma real de execucao.""" + monkeypatch.setattr(build_metadata, "is_running_from_source", lambda: False) + monkeypatch.setattr(build_info, "BUILD_PLATFORM", "macOS") + monkeypatch.setattr(build_info, "BUILD_MACOS_VERSION", "15.0 (24A335)") + monkeypatch.setattr(build_metadata.platform, "system", lambda: "Windows") + + assert "macOS" not in build_metadata.get_footer_build_label() + + +def test_footer_shows_dev_label_when_running_from_source(monkeypatch): + monkeypatch.setattr(build_metadata, "is_running_from_source", lambda: True) + monkeypatch.setattr(build_metadata.platform, "system", lambda: "Windows") + + label = build_metadata.get_footer_build_label() + + assert "macOS" not in label + assert "Windows" in label \ No newline at end of file From 94fc63848117b213075517cd1f9a2ca01df961cd Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:58:02 -0300 Subject: [PATCH 32/36] feat(db): adiciona list_articles/count_articles/get_package_name - Novas funcoes espelhando list_validations/count_validations, mas com JOIN entre package_article_snapshot e package_validation_history; filtros por nome de pacote, doi, pid, status do artigo e history_id. - get_package_name(): lookup simples pra exibir o nome do pacote quando a aba de Artigos estiver filtrada por history_id. Porque: primeiro passo pra atender a 2a rodada de review do PR #37 (pitangainnovare) - a aba "Artigos" precisa listar todos os snapshots de artigos, nao so os do ultimo pacote selecionado, com filtros proprios. O schema ja suporta isso sem migracao (history_id ja e chave estrangeira em package_article_snapshot). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- .../src/spsvalidator/db/repository.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/spsvalidator/src/spsvalidator/db/repository.py b/spsvalidator/src/spsvalidator/db/repository.py index e27d1f3..dd290f7 100644 --- a/spsvalidator/src/spsvalidator/db/repository.py +++ b/spsvalidator/src/spsvalidator/db/repository.py @@ -175,6 +175,109 @@ def count_validations( return int(total) +def _articles_filter_clause( + name_query: str | None, + doi_query: str | None, + pid_query: str | None, + status: str | None, + history_id: str | None, +) -> tuple[str, list]: + conditions = [] + params: list = [] + if name_query: + conditions.append("LOWER(package_validation_history.package_name) LIKE LOWER(?)") + params.append(f"%{name_query}%") + if doi_query: + conditions.append("LOWER(package_article_snapshot.doi) LIKE LOWER(?)") + params.append(f"%{doi_query}%") + if pid_query: + conditions.append("LOWER(package_article_snapshot.pid) LIKE LOWER(?)") + params.append(f"%{pid_query}%") + if status: + conditions.append("package_article_snapshot.article_status = ?") + params.append(status) + if history_id: + conditions.append("package_article_snapshot.history_id = ?") + params.append(history_id) + where_clause = " WHERE " + " AND ".join(conditions) if conditions else "" + return where_clause, params + + +def list_articles( + db_path: str, + name_query: str | None = None, + doi_query: str | None = None, + pid_query: str | None = None, + status: str | None = None, + history_id: str | None = None, + limit: int | None = None, + offset: int = 0, +) -> list[dict]: + where_clause, params = _articles_filter_clause( + name_query, doi_query, pid_query, status, history_id + ) + sql = f""" + SELECT + package_article_snapshot.id, + package_article_snapshot.history_id, + package_article_snapshot.xml_path, + package_article_snapshot.title, + package_article_snapshot.authors_text, + package_article_snapshot.doi, + package_article_snapshot.pid, + package_article_snapshot.article_status, + package_article_snapshot.issue_count, + package_validation_history.package_name, + package_validation_history.validated_at + FROM package_article_snapshot + JOIN package_validation_history + ON package_validation_history.id = package_article_snapshot.history_id + {where_clause} + ORDER BY datetime(package_validation_history.validated_at) DESC, + package_article_snapshot.xml_path + """ + if limit is not None: + sql += " LIMIT ? OFFSET ?" + params = params + [limit, offset] + + with sqlite3.connect(db_path) as connection: + connection.row_factory = sqlite3.Row + rows = connection.execute(sql, params).fetchall() + return [dict(row) for row in rows] + + +def count_articles( + db_path: str, + name_query: str | None = None, + doi_query: str | None = None, + pid_query: str | None = None, + status: str | None = None, + history_id: str | None = None, +) -> int: + where_clause, params = _articles_filter_clause( + name_query, doi_query, pid_query, status, history_id + ) + sql = f""" + SELECT COUNT(*) + FROM package_article_snapshot + JOIN package_validation_history + ON package_validation_history.id = package_article_snapshot.history_id + {where_clause} + """ + with sqlite3.connect(db_path) as connection: + total = connection.execute(sql, params).fetchone()[0] + return int(total) + + +def get_package_name(db_path: str, history_id: str) -> str | None: + with sqlite3.connect(db_path) as connection: + row = connection.execute( + "SELECT package_name FROM package_validation_history WHERE id = ?", + (history_id,), + ).fetchone() + return row[0] if row else None + + def get_validation_details(db_path: str, history_id: str) -> dict | None: with sqlite3.connect(db_path) as connection: connection.row_factory = sqlite3.Row From 43b0294357c8743c97b69561766b8b4a8f255477 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:58:10 -0300 Subject: [PATCH 33/36] test(db): cobre list_articles/count_articles - Testes de join com nome/data do pacote, filtro por history_id, nome de pacote, doi, pid, status, paginacao (limit/offset) e contagem. Porque: fixa em teste o comportamento das funcoes adicionadas no commit anterior antes de conecta-las as rotas. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- .../tests/test_articles_repository.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 spsvalidator/tests/test_articles_repository.py diff --git a/spsvalidator/tests/test_articles_repository.py b/spsvalidator/tests/test_articles_repository.py new file mode 100644 index 0000000..ed108f3 --- /dev/null +++ b/spsvalidator/tests/test_articles_repository.py @@ -0,0 +1,123 @@ +from spsvalidator.db.repository import ( + count_articles, + init_db, + insert_validation_result, + list_articles, +) + + +def _insert(db_path, package_name, articles): + return insert_validation_result( + db_path, + package_name=package_name, + package_sha256=f"sha-{package_name}", + rows=[], + exceptions=[], + articles=articles, + status="valid", + ) + + +def _seed(db_path): + history_a = _insert( + db_path, + "1676-0611-bn-rpass-11-26-1", + [ + { + "xml_path": "art01.xml", + "title": "Artigo A", + "authors_text": "Autor A", + "doi": "10.1590/aaa", + "pid": "S1676-06112026000100001", + "article_status": "ok", + "issue_count": 0, + } + ], + ) + history_b = _insert( + db_path, + "1234-5678-ean-outro-pacote", + [ + { + "xml_path": "art02.xml", + "title": "Artigo B", + "authors_text": "Autor B", + "doi": "10.1590/bbb", + "pid": "S1234-56782026000200002", + "article_status": "issue", + "issue_count": 3, + } + ], + ) + return history_a, history_b + + +def test_list_articles_joins_package_name_and_validated_at(tmp_path): + db_path = str(tmp_path / "test.sqlite3") + init_db(db_path) + _seed(db_path) + + articles = list_articles(db_path) + + assert len(articles) == 2 + assert {a["package_name"] for a in articles} == { + "1676-0611-bn-rpass-11-26-1", + "1234-5678-ean-outro-pacote", + } + assert all(a["validated_at"] for a in articles) + + +def test_list_articles_filters_by_history_id(tmp_path): + db_path = str(tmp_path / "test.sqlite3") + init_db(db_path) + history_a, history_b = _seed(db_path) + + articles = list_articles(db_path, history_id=history_a) + + assert len(articles) == 1 + assert articles[0]["doi"] == "10.1590/aaa" + + +def test_list_articles_filters_by_package_name(tmp_path): + db_path = str(tmp_path / "test.sqlite3") + init_db(db_path) + _seed(db_path) + + articles = list_articles(db_path, name_query="ean") + + assert len(articles) == 1 + assert articles[0]["package_name"] == "1234-5678-ean-outro-pacote" + + +def test_list_articles_filters_by_doi_pid_and_status(tmp_path): + db_path = str(tmp_path / "test.sqlite3") + init_db(db_path) + _seed(db_path) + + assert len(list_articles(db_path, doi_query="bbb")) == 1 + assert len(list_articles(db_path, pid_query="S1676")) == 1 + assert len(list_articles(db_path, status="issue")) == 1 + assert len(list_articles(db_path, status="ok")) == 1 + + +def test_list_articles_respects_limit_and_offset(tmp_path): + db_path = str(tmp_path / "test.sqlite3") + init_db(db_path) + _seed(db_path) + + first_page = list_articles(db_path, limit=1, offset=0) + second_page = list_articles(db_path, limit=1, offset=1) + + assert len(first_page) == 1 + assert len(second_page) == 1 + assert first_page[0]["id"] != second_page[0]["id"] + + +def test_count_articles_matches_filters(tmp_path): + db_path = str(tmp_path / "test.sqlite3") + init_db(db_path) + _seed(db_path) + + assert count_articles(db_path) == 2 + assert count_articles(db_path, status="issue") == 1 + assert count_articles(db_path, name_query="nao-existe") == 0 \ No newline at end of file From 17c172ad4129b45fce1ae4bc732eb789c898a23d Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:58:19 -0300 Subject: [PATCH 34/36] feat(web): adiciona /articles-list e desacopla a aba Artigos de latest_result - Nova `_paginated_articles()` (mesmo padrao de `_paginated_history()`), endpoint `/articles-list` pra paginacao/filtro via AJAX, parametros de querystring com prefixo `article_` pra nao colidir com os do historico de pacotes (q/status/page/page_size). - `index()` simplificado: nao usa mais get_validation_details/ latest_result pra decidir o que mostrar; so calcula `default_tab` ("articles" quando history_id esta na query, "history" caso contrario) - a aba Artigos passa a ser sempre calculada e sempre disponivel. - `history_id` na querystring agora e um filtro da aba Artigos (via _paginated_articles), nao mais um gatilho pra carregar um pacote especifico. - Redirect apos validar preserva tambem os parametros article_* (mesma logica ja aplicada a q/status/page/page_size). Porque: pedido explicito da 2a rodada de review do PR #37 (pitangainnovare) - "Articles" deve sempre listar todos os snapshots (JOIN ja existente, sem migracao), ordenados por data, e ao clicar num pacote deve abrir Artigos filtrado por history_id, com filtro removivel. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- spsvalidator/src/spsvalidator/web/routes.py | 79 ++++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/spsvalidator/src/spsvalidator/web/routes.py b/spsvalidator/src/spsvalidator/web/routes.py index 7bde58a..ebd23b6 100644 --- a/spsvalidator/src/spsvalidator/web/routes.py +++ b/spsvalidator/src/spsvalidator/web/routes.py @@ -18,8 +18,11 @@ from packtools import catalogs from spsvalidator.db.repository import ( + count_articles, count_validations, + get_package_name, get_validation_details, + list_articles, list_validations, ) from spsvalidator.domain.export import build_validation_csv @@ -176,9 +179,60 @@ def _paginated_history() -> dict: } +def _paginated_articles() -> dict: + db_path = current_app.config["DB_PATH"] + name_query = request.args.get("article_q", "").strip() + doi_query = request.args.get("article_doi", "").strip() + pid_query = request.args.get("article_pid", "").strip() + status_query = request.args.get("article_status", "").strip() + history_id = request.args.get("history_id", "").strip() + page_size = _parse_int(request.args.get("article_page_size"), DEFAULT_PAGE_SIZE) + page_size = min(MAX_PAGE_SIZE, max(1, page_size)) + page = max(1, _parse_int(request.args.get("article_page"), 1)) + + total = count_articles( + db_path, name_query, doi_query, pid_query, status_query, history_id or None + ) + total_pages = max(1, -(-total // page_size)) # ceil division + page = min(page, total_pages) + + articles = list_articles( + db_path, + name_query, + doi_query, + pid_query, + status_query, + history_id or None, + limit=page_size, + offset=(page - 1) * page_size, + ) + for article in articles: + article["validated_at"] = _format_validated_at(article["validated_at"]) + + return { + "articles": articles, + "article_name_query": name_query, + "article_doi_query": doi_query, + "article_pid_query": pid_query, + "article_status_query": status_query, + "article_history_id": history_id, + "article_page": page, + "article_page_size": page_size, + "article_total": total, + "article_total_pages": total_pages, + "article_page_range": _page_range(page, total_pages), + "selected_package_name": ( + get_package_name(db_path, history_id) if history_id else None + ), + } + + def _render_index(**context): context.setdefault("error_message", None) - return render_template("index.html", **_paginated_history(), **context) + context.setdefault("default_tab", "history") + return render_template( + "index.html", **_paginated_history(), **_paginated_articles(), **context + ) @web_blueprint.get("/history-list") @@ -186,15 +240,15 @@ def history_list(): return render_template("_history_list.html", **_paginated_history()) +@web_blueprint.get("/articles-list") +def articles_list(): + return render_template("_articles_list.html", **_paginated_articles()) + + @web_blueprint.get("/") def index(): - selected_id = request.args.get("history_id") - details = ( - get_validation_details(current_app.config["DB_PATH"], selected_id) - if selected_id - else None - ) - return _render_index(latest_result=details) + selected_id = request.args.get("history_id", "").strip() + return _render_index(default_tab="articles" if selected_id else "history") @web_blueprint.post("/validate") @@ -203,7 +257,6 @@ def validate(): if uploaded_file is None or not uploaded_file.filename: return _render_index( - latest_result=None, error_message=gettext("Selecione um arquivo .zip para validar."), ) @@ -216,7 +269,7 @@ def validate(): html_asset_urls=_html_preview_asset_urls(), ) except Exception as exc: - return _render_index(latest_result=None, error_message=str(exc)) + return _render_index(error_message=str(exc)) return redirect( url_for( @@ -226,6 +279,12 @@ def validate(): status=request.args.get("status") or None, page_size=request.args.get("page_size") or None, page=request.args.get("page") or None, + article_q=request.args.get("article_q") or None, + article_doi=request.args.get("article_doi") or None, + article_pid=request.args.get("article_pid") or None, + article_status=request.args.get("article_status") or None, + article_page_size=request.args.get("article_page_size") or None, + article_page=request.args.get("article_page") or None, ) ) From 5aadd559d6483b12ba5f0a25a2fab166fd427b33 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:58:25 -0300 Subject: [PATCH 35/36] feat(web): cria partial da listagem de artigos - Espelha _history_list.html: tabela com pacote/data/xml/titulo/ autores/doi/pid/status, paginacao numerada, mensagem de "nenhum artigo encontrado" sensivel aos filtros ativos, e chip "Filtrando por pacote: X" com link "Limpar filtro" quando history_id esta setado. Porque: parte do redesenho da aba Artigos pedido na 2a rodada de review do PR #37; usado tanto no carregamento inicial de `/` quanto no endpoint `/articles-list` (AJAX), igual ao papel de _history_list.html pro historico de pacotes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- .../web/templates/_articles_list.html | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 spsvalidator/src/spsvalidator/web/templates/_articles_list.html diff --git a/spsvalidator/src/spsvalidator/web/templates/_articles_list.html b/spsvalidator/src/spsvalidator/web/templates/_articles_list.html new file mode 100644 index 0000000..107b914 --- /dev/null +++ b/spsvalidator/src/spsvalidator/web/templates/_articles_list.html @@ -0,0 +1,85 @@ +{% set article_status_labels = {"ok": _("OK"), "issue": _("Com ocorrências")} %} +{% if article_history_id %} +

+ {{ _("Filtrando por pacote: %(name)s", name=selected_package_name or article_history_id) }} + {{ _("Limpar filtro") }} +

+{% endif %} +{% if articles %} +
+ + + + + + + + + + + + + + + {% for article in articles %} + + + + + + + + + + + {% endfor %} + +
{{ _("Pacote") }}{{ _("Data da validação") }}{{ _("Arquivo XML") }}{{ _("Título") }}{{ _("Autores") }}DOIPID{{ _("Status") }}
{{ article.package_name }}{{ article.validated_at }}{{ article.xml_path }}{{ article.title }}{{ article.authors_text }}{{ article.doi }}{{ article.pid }}{{ article_status_labels.get(article.article_status, article.article_status) }}
+
+{% else %} +{% if article_name_query or article_doi_query or article_pid_query or article_status_query or article_history_id %} +

{{ _("Nenhum artigo encontrado com esse filtro.") }}

+{% else %} +

{{ _("Nenhum artigo validado ainda.") }}

+{% endif %} +{% endif %} +{% if article_total > 0 %} + +{% endif %} \ No newline at end of file From b6c2e541e21fd1b7c3ca1ecee2dfa1a694c278d8 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Sun, 2 Aug 2026 12:58:41 -0300 Subject: [PATCH 36/36] feat(web): abas Pacotes/Artigos sempre visiveis com filtro proprio de artigos - Remove o gate `{% if latest_result %}` que escondia as abas e o conteudo de Artigos quando nenhum pacote estava selecionado; a aba ativa por padrao agora vem de `default_tab` (calculado nas rotas). - Adiciona formulario de filtro da aba Artigos (pacote/doi/pid/status/ itens por pagina), espelhando o formulario ja existente pro historico de pacotes. - Generaliza `bindLiveHistoryFilters()` em `bindLiveFilters(config)`, reaproveitado pras duas abas; corrige o bug de `window.history. replaceState` sobrescrever a querystring inteira em vez de mesclar - cada bloco agora parte de `new URLSearchParams(window.location. search)` e so altera os proprios campos, entao filtrar/paginar numa aba nao apaga o estado da outra na URL. - Adiciona estilos `.status-ok`/`.status-issue` (badges da aba Artigos) e `.active-filter` (chip de filtro por pacote). Porque: fecha a 2a rodada de review do PR #37 (pitangainnovare) - "Articles" precisa ser uma lista sempre visivel e completa, nao um detalhe do ultimo pacote validado. Testado com Playwright headless contra dados reais (filtro por nome/doi/pid/status, clique em pacote, limpar filtro, preservacao cruzada de querystring entre as abas) - screenshots conferidos visualmente antes deste commit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015N4tShpu6YUfgyPJR1PKnD --- .../src/spsvalidator/web/templates/index.html | 167 +++++++++++------- 1 file changed, 101 insertions(+), 66 deletions(-) diff --git a/spsvalidator/src/spsvalidator/web/templates/index.html b/spsvalidator/src/spsvalidator/web/templates/index.html index 9bd8914..b446c60 100644 --- a/spsvalidator/src/spsvalidator/web/templates/index.html +++ b/spsvalidator/src/spsvalidator/web/templates/index.html @@ -141,8 +141,11 @@ font-size: 11px; font-weight: 700; } - .status-valid .badge { background: var(--success-soft); color: var(--success); } - .status-invalid .badge, .status-error .badge { background: var(--critical-soft); color: var(--critical); } + .status-valid .badge, .status-ok .badge { background: var(--success-soft); color: var(--success); } + .status-invalid .badge, .status-error .badge, .status-issue .badge { background: var(--critical-soft); color: var(--critical); } + + .active-filter { margin-bottom: 12px; } + .active-filter a { margin-left: 8px; } .actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; } .action-group { margin-bottom: 2px; } @@ -329,16 +332,12 @@

{{ _("Validar pacote SPS") }}

- {% if latest_result %}
- - + +
- {% else %} -

{{ _("Pacotes validados") }}

- {% endif %} -
+
{{ _("Pacotes validados") }}
- {% if latest_result %} -
-
- - - - - - - - - - - - - {% for article in latest_result.articles %} - - - - - - - - - {% endfor %} - -
{{ _("Arquivo XML") }}{{ _("Título") }}{{ _("Autores") }}DOIPID{{ _("Status") }}
{{ article.xml_path }}{{ article.title }}{{ article.authors_text }}{{ article.doi }}{{ article.pid }}{{ article.article_status }}
+
+ + + + + + + + + {% if article_name_query or article_doi_query or article_pid_query or article_status_query %} + {{ _("Limpar") }} + {% endif %} + +
+ {% include "_articles_list.html" %}
- {% endif %}
@@ -532,32 +544,31 @@

{{ _("Pacotes validados") }}

bindCsvDownloads(); window.addEventListener("pywebviewready", bindCsvDownloads); - function bindLiveHistoryFilters() { - var form = document.getElementById("history-filters"); - var resultsContainer = document.getElementById("history-list"); + function bindLiveFilters(config) { + var form = document.getElementById(config.formId); + var resultsContainer = document.getElementById(config.containerId); if (!form || !resultsContainer) { return; } - var searchInput = form.querySelector("input[name='q']"); - var statusSelect = form.querySelector("select[name='status']"); - var pageSizeInput = form.querySelector("input[name='page_size']"); + var pageSizeInput = form.querySelector(config.pageSizeSelector); var debounceTimer = null; - var currentPage = 1; function applyFilters(targetPage) { - currentPage = targetPage || 1; var pageSize = Math.min(100, Math.max(1, parseInt(pageSizeInput.value, 10) || 25)); - var params = new URLSearchParams(); - if (searchInput.value.trim()) { - params.set("q", searchInput.value.trim()); - } - if (statusSelect.value) { - params.set("status", statusSelect.value); - } - params.set("page_size", pageSize); - params.set("page", currentPage); + var params = new URLSearchParams(window.location.search); + config.fields.forEach(function (field) { + var input = form.querySelector(field.selector); + var value = input.value.trim(); + if (value) { + params.set(field.name, value); + } else { + params.delete(field.name); + } + }); + params.set(config.pageSizeParam, pageSize); + params.set(config.pageParam, targetPage || 1); var queryString = params.toString(); - fetch("/history-list?" + queryString) + fetch(config.endpoint + "?" + queryString) .then(function (response) { return response.text(); }) @@ -573,16 +584,14 @@

{{ _("Pacotes validados") }}

applyFilters(1); }); - searchInput.addEventListener("input", function () { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(function () { - applyFilters(1); - }, 350); - }); - - statusSelect.addEventListener("change", function () { - clearTimeout(debounceTimer); - applyFilters(1); + config.fields.forEach(function (field) { + var input = form.querySelector(field.selector); + input.addEventListener(field.liveEvent || "input", function () { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function () { + applyFilters(1); + }, 350); + }); }); pageSizeInput.addEventListener("input", function () { @@ -602,7 +611,33 @@

{{ _("Pacotes validados") }}

}); } - bindLiveHistoryFilters(); + bindLiveFilters({ + formId: "history-filters", + containerId: "history-list", + endpoint: "/history-list", + pageParam: "page", + pageSizeParam: "page_size", + pageSizeSelector: "input[name='page_size']", + fields: [ + { name: "q", selector: "input[name='q']" }, + { name: "status", selector: "select[name='status']", liveEvent: "change" }, + ], + }); + + bindLiveFilters({ + formId: "article-filters", + containerId: "articles-list", + endpoint: "/articles-list", + pageParam: "article_page", + pageSizeParam: "article_page_size", + pageSizeSelector: "input[name='article_page_size']", + fields: [ + { name: "article_q", selector: "input[name='article_q']" }, + { name: "article_doi", selector: "input[name='article_doi']" }, + { name: "article_pid", selector: "input[name='article_pid']" }, + { name: "article_status", selector: "select[name='article_status']", liveEvent: "change" }, + ], + });