Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ Versioning follows [SemVer](https://semver.org/): **MAJOR.MINOR.PATCH**

---

## [1.38.4] — 2026-07-28

### Added
- **"Ver novidades" mesmo já atualizado.** A tela de Atualizações mostrava as
notas de versão só enquanto havia uma atualização pendente — quem já estava na
última versão via apenas "Sistema atualizado", sem nenhum jeito de reler o que
mudou nela. Agora, estando em dia, um link mostra as notas da própria versão
instalada (ou, se não puder carregá-las, leva para a página de releases no
GitHub).

## [1.38.3] — 2026-07-28

### Changed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.38.3
1.38.4
45 changes: 45 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,51 @@ def cumulative_release_notes():
return latest_release_notes(), False


_current_notes_cache = {"version": None, "notes": ""}


def _version_just_below(version):
"""String cujo _version_tuple compara MENOR que o de `version`, mas maior que
qualquer versão anterior real publicada — usado como limite inferior exclusivo
em _slice_changelog p/ isolar só a seção da própria `version` (decrementa o
componente mais à direita com folga; toma emprestado do componente à esquerda
quando ele já é 0, sem nunca produzir um número negativo)."""
t = list(_version_tuple(version))
while len(t) < 3:
t.append(0)
major, minor, patch = t[0], t[1], t[2]
if patch > 0:
return f"{major}.{minor}.{patch - 1}"
if minor > 0:
return f"{major}.{minor - 1}.999999"
if major > 0:
return f"{major - 1}.999999.999999"
return "0.0.0"


def current_release_notes():
"""Notas do CHANGELOG só da versão JÁ INSTALADA — para quem está em dia poder
reler o que mudou nesta versão mesmo sem update pendente (a página de update
só mostra o card acumulado quando `latest` é mais nova que `current`). Fonte:
o CHANGELOG.md cru na tag da PRÓPRIA versão instalada. Reusa _slice_changelog
(latest=current, limite inferior logo abaixo via _version_just_below) — não
duplica o parser nem o renderizador. Fail-open: tag inexistente, rede fora do
ar, ou nada casado → '' (o template cai pro link do GitHub). Cacheada por
versão instalada, como cumulative_release_notes cacheia por (current, tag)."""
current = current_version()
if _current_notes_cache["version"] == current and _current_notes_cache["notes"]:
return _current_notes_cache["notes"]
try:
md = _changelog_md(f"v{current}")
notes = _slice_changelog(md, _version_just_below(current), current)
if notes:
_current_notes_cache.update(version=current, notes=notes)
return notes
except Exception:
log.info("github_changelog.current_unavailable", exc_info=True)
return ""


def notes_incomplete_warning(complete, current, latest):
"""Decide se vale mostrar o aviso de "histórico pode estar incompleto":
só quando `complete` é False (cumulative_release_notes caiu no fallback) E o
Expand Down
15 changes: 12 additions & 3 deletions routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
app, admin_required, demo_blocked, t, MIN_PASSWORD_LEN, DEMO_MODE,
BRANDS_DIR, _fetch_brand_logo, _clean_domain, public_base_url,
RELEASES_URL, check_latest_release, current_version, _version_tuple,
cumulative_release_notes, render_release_notes, notes_incomplete_warning,
cumulative_release_notes, current_release_notes, render_release_notes,
notes_incomplete_warning,
)

log = log_cfg.get_logger()
Expand Down Expand Up @@ -222,20 +223,28 @@ def admin_update():
# nem depende mais do limite de 60/h.
latest = check_latest_release(max_age=60)
cur = current_version()
update_available = bool(latest) and _version_tuple(latest) > _version_tuple(cur)
release_notes = ""
notes_incomplete = False
if latest:
current_notes = ""
if update_available:
notes, complete = cumulative_release_notes()
release_notes = render_release_notes(notes)
notes_incomplete = notes_incomplete_warning(complete, cur, latest)
elif latest:
# Já atualizado: sem update pendente, então nada de cumulative_release_notes
# (evita um fetch à toa) — mostra as notas da PRÓPRIA versão instalada, se
# o CHANGELOG na tag dela estiver disponível.
current_notes = render_release_notes(current_release_notes())
return render_template(
"admin/update.html",
current=cur,
latest=latest,
update_available=bool(latest) and _version_tuple(latest) > _version_tuple(cur),
update_available=update_available,
releases_url=RELEASES_URL,
release_notes=release_notes,
notes_incomplete=notes_incomplete,
current_notes=current_notes,
)


Expand Down
17 changes: 17 additions & 0 deletions templates/admin/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,24 @@ <h4 class="fw-bold mb-4">{{ _('Atualizações') }}</h4>
{% else %}
<div class="alert alert-success mb-0">
<i class="bi bi-check-circle-fill me-1"></i>{{ _('Sistema atualizado') }}.
{% if current_notes %}
<a data-bs-toggle="collapse" href="#currentNotes" role="button"
aria-expanded="false" aria-controls="currentNotes">{{ _('Ver novidades desta versão') }}</a>
{% else %}
<a href="{{ releases_url }}" target="_blank" rel="noopener">{{ _('Ver novidades desta versão') }}</a>
{% endif %}
</div>
{% if current_notes %}
<div class="collapse mt-3" id="currentNotes">
<div class="border rounded p-3 bg-body-tertiary release-notes"
style="max-height:50vh;overflow:auto;font-size:.85rem">
<div class="text-muted mb-2" style="font-size:.8rem">
<i class="bi bi-stars me-1"></i>{{ _('Novidades da v{v}').replace('{v}', current) }}
</div>
{{ current_notes }}
</div>
</div>
{% endif %}
{% endif %}

<div id="updateProgress" class="mt-3 d-none">
Expand Down
56 changes: 56 additions & 0 deletions tests/test_ui_v138.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Coluna "Cor" na lista de rolos.
- "Ver novidades" acumulado (CHANGELOG entre a versão instalada e a última).
"""
import time
from datetime import date

from conftest import ADMIN_USER, ADMIN_PASS
Expand Down Expand Up @@ -180,3 +181,58 @@ def test_update_page_hides_warning_when_slicing_works(app_module, auth_client, m
html = auth_client.get("/admin/update").get_data(as_text=True)
assert "recurso novo" in html # notas acumuladas carregadas de verdade
assert "histórico completo" not in html


# ── T-731: "Ver novidades" mesmo já atualizado (notas da versão instalada) ───
#
# Mesma armadilha do bloco acima: routes/admin.py faz `from app import
# current_version` (nome próprio), então monkeypatchar app_module.current_version
# não muda o que a rota lê. Usa-se a versão REAL (app_module.current_version()).

def test_current_release_notes_returns_only_installed_version(app_module, monkeypatch):
cur = app_module.current_version()
changelog = (
"# Changelog\n\n"
f"## [{cur}] — 2026-07-28\n### Added\n- recurso da instalada\n\n"
"## [1.30.0] — 2026-01-01\n### Added\n- recurso antigo\n"
)
app_module._current_notes_cache.update(version=None, notes="")
monkeypatch.setattr(app_module, "_changelog_md", lambda tag: changelog)
notes = app_module.current_release_notes()
assert "recurso da instalada" in notes
assert "recurso antigo" not in notes
assert f"[{cur}]" in notes


def test_current_release_notes_fails_open_on_error(app_module, monkeypatch):
app_module._current_notes_cache.update(version=None, notes="")
monkeypatch.setattr(app_module, "_changelog_md",
lambda tag: (_ for _ in ()).throw(OSError("sem rede")))
assert app_module.current_release_notes() == ""


def test_update_page_shows_current_version_notes_when_up_to_date(app_module, auth_client, monkeypatch):
cur = app_module.current_version()
changelog = (
"# Changelog\n\n"
f"## [{cur}] — 2026-07-28\n### Added\n- recurso desta versao instalada\n"
)
# Já atualizado: latest == current (nenhuma atualização pendente).
app_module._release_cache.update(tag=cur, ts=time.time(), ok=True)
app_module._current_notes_cache.update(version=None, notes="")
monkeypatch.setattr(app_module, "_changelog_md", lambda tag: changelog)
html = auth_client.get("/admin/update").get_data(as_text=True)
assert "recurso desta versao instalada" in html


def test_update_page_falls_back_to_github_link_when_current_notes_fail(app_module, auth_client, monkeypatch):
cur = app_module.current_version()
app_module._release_cache.update(tag=cur, ts=time.time(), ok=True)
app_module._current_notes_cache.update(version=None, notes="")
monkeypatch.setattr(app_module, "_changelog_md",
lambda tag: (_ for _ in ()).throw(OSError("sem rede")))
resp = auth_client.get("/admin/update")
html = resp.get_data(as_text=True)
assert resp.status_code == 200
assert app_module.RELEASES_URL in html
assert "Traceback" not in html
2 changes: 2 additions & 0 deletions translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"Novidades (v{de} → v{ate})": "What's new (v{de} → v{ate})",
"Atualizar agora": "Update now",
"Sistema atualizado": "System up to date",
"Ver novidades desta versão": "See what's new in this version",
"Atualizado para a v{v} com sucesso": "Successfully updated to v{v}",
"Atualizando… o serviço será reiniciado, aguarde.":
"Updating… the service will restart, please wait.",
Expand Down Expand Up @@ -526,6 +527,7 @@
"Novidades (v{de} → v{ate})": "Novedades (v{de} → v{ate})",
"Atualizar agora": "Actualizar ahora",
"Sistema atualizado": "Sistema actualizado",
"Ver novidades desta versão": "Ver novedades de esta versión",
"Atualizado para a v{v} com sucesso": "Actualizado a la v{v} con éxito",
"Atualizando… o serviço será reiniciado, aguarde.":
"Actualizando… el servicio se reiniciará, espera.",
Expand Down