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.3] — 2026-07-28

### Changed
- **Aviso quando o histórico de novidades não pôde ser carregado por completo.**
A tela de Atualizações mostra as notas de TODAS as versões entre a instalada e a
mais recente; se o GitHub estiver indisponível e o card cair para a nota de só a
última release, agora aparece um aviso discreto avisando que o histórico pode
estar incompleto, com um link para ver tudo no GitHub. Sem aviso quando a
atualização é só um patch acima (aí a última nota já é a história inteira).

## [1.38.2] — 2026-07-10

### Security
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.38.2
1.38.3
31 changes: 26 additions & 5 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,22 +476,43 @@ def cumulative_release_notes():
entre a versão instalada (exclusive) e a última publicada (inclusive) — assim
quem está várias versões atrás vê o histórico todo, não só a última. Fonte: o
CHANGELOG.md cru na tag latest. Fail-open: sem tag, ou se a busca/parse falhar
ou nada casar, cai nas notas da última release (comportamento anterior)."""
ou nada casar, cai nas notas da última release (comportamento anterior).

Devolve (notes, complete): `complete` é True só quando `notes` veio de fatiar
o CHANGELOG de verdade; é False em todo caminho de fallback (sem tag, busca
falhou, ou o corte não achou nada) — quem chama usa isso para avisar que o
histórico mostrado pode estar incompleto."""
tag = _release_cache.get("tag")
current = current_version()
if not tag:
return latest_release_notes()
return latest_release_notes(), False
key = (current, tag)
if _cumulative_cache["key"] == key and _cumulative_cache["notes"]:
return _cumulative_cache["notes"]
return _cumulative_cache["notes"], True
try:
notes = _slice_changelog(_changelog_md(f"v{tag}"), current, tag)
if notes:
_cumulative_cache.update(key=key, notes=notes)
return notes
return notes, True
except Exception:
log.info("github_changelog.unavailable", exc_info=True)
return latest_release_notes()
return latest_release_notes(), False


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
salto não é um patch único logo acima da instalada (mesmo major.minor,
latest_patch - current_patch == 1) — nesse caso a nota da última release JÁ
é a história inteira, e o aviso seria só ruído."""
if complete:
return False
cur_t, lat_t = _version_tuple(current), _version_tuple(latest)
if (len(cur_t) >= 3 and len(lat_t) >= 3
and cur_t[0] == lat_t[0] and cur_t[1] == lat_t[1]
and lat_t[2] - cur_t[2] == 1):
return False
return True


# Subconjunto de Markdown usado nas release notes → HTML seguro, sem dependência
Expand Down
11 changes: 9 additions & 2 deletions routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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,
cumulative_release_notes, render_release_notes, notes_incomplete_warning,
)

log = log_cfg.get_logger()
Expand Down Expand Up @@ -222,13 +222,20 @@ def admin_update():
# nem depende mais do limite de 60/h.
latest = check_latest_release(max_age=60)
cur = current_version()
release_notes = ""
notes_incomplete = False
if latest:
notes, complete = cumulative_release_notes()
release_notes = render_release_notes(notes)
notes_incomplete = notes_incomplete_warning(complete, cur, latest)
return render_template(
"admin/update.html",
current=cur,
latest=latest,
update_available=bool(latest) and _version_tuple(latest) > _version_tuple(cur),
releases_url=RELEASES_URL,
release_notes=render_release_notes(cumulative_release_notes()) if latest else "",
release_notes=release_notes,
notes_incomplete=notes_incomplete,
)


Expand Down
7 changes: 7 additions & 0 deletions templates/admin/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ <h4 class="fw-bold mb-4">{{ _('Atualizações') }}</h4>
<div class="text-muted mb-2" style="font-size:.8rem">
<i class="bi bi-stars me-1"></i>{{ _('Novidades (v{de} → v{ate})').replace('{de}', current).replace('{ate}', latest) }}
</div>
{% if notes_incomplete %}
<div class="text-muted mb-2" style="font-size:.8rem">
<i class="bi bi-exclamation-triangle me-1"></i>
{{ _('Não foi possível carregar o histórico completo; mostrando só as notas da última versão.') }}
<a href="{{ releases_url }}" target="_blank" rel="noopener">{{ _('Ver tudo no GitHub') }}</a>
</div>
{% endif %}
{{ release_notes }}
</div>
</div>
Expand Down
63 changes: 60 additions & 3 deletions tests/test_ui_v138.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,78 @@ def test_cumulative_notes_slices_from_changelog(app_module, monkeypatch):
app_module._release_cache.update(tag="1.37.0", ts=0.0, ok=True)
app_module._cumulative_cache.update(key=None, notes="")
monkeypatch.setattr(app_module, "_changelog_md", lambda tag: _SAMPLE_CHANGELOG)
notes = app_module.cumulative_release_notes()
notes, complete = app_module.cumulative_release_notes()
assert complete is True
assert "recurso do 1.37" in notes and "recurso do 1.36" in notes
assert "recurso do 1.35" not in notes
# mais de uma seção de versão presente — é o histórico acumulado, não só a última
assert "[1.37.0]" in notes and "[1.36.0]" in notes


def test_cumulative_notes_fallback_when_no_tag(app_module, monkeypatch):
app_module._release_cache.update(tag=None, ts=0.0, ok=False)
monkeypatch.setattr(app_module, "latest_release_notes", lambda: "notas da ultima")
assert app_module.cumulative_release_notes() == "notas da ultima"
notes, complete = app_module.cumulative_release_notes()
assert notes == "notas da ultima"
assert complete is False


def test_cumulative_notes_fallback_on_fetch_error(app_module, monkeypatch):
monkeypatch.setattr(app_module, "current_version", lambda: "1.30.0")
app_module._release_cache.update(tag="1.37.0", ts=0.0, ok=True)
app_module._cumulative_cache.update(key=None, notes="")
monkeypatch.setattr(app_module, "_changelog_md",
lambda tag: (_ for _ in ()).throw(OSError("sem rede")))
monkeypatch.setattr(app_module, "latest_release_notes", lambda: "notas da ultima")
assert app_module.cumulative_release_notes() == "notas da ultima"
notes, complete = app_module.cumulative_release_notes()
# fail-open preservado: cai pra nota da última release, mas avisa que é incompleto
assert notes == "notas da ultima"
assert complete is False


# ── Aviso "histórico incompleto": só quando vale a pena avisar ───────────────

def test_no_warning_for_single_patch_bump(app_module):
# 1.38.2 → 1.38.3 é um patch só acima: a nota da última release JÁ é a
# história inteira, avisar seria ruído mesmo com complete=False.
assert app_module.notes_incomplete_warning(False, "1.38.2", "1.38.3") is False


def test_warning_for_bigger_gap(app_module):
assert app_module.notes_incomplete_warning(False, "1.30.0", "1.37.0") is True


def test_no_warning_when_notes_are_complete(app_module):
assert app_module.notes_incomplete_warning(True, "1.30.0", "1.37.0") is False


# ── /admin/update: o aviso aparece só quando o fallback foi mesmo acionado ────
#
# Nota: `routes/admin.py` importa `current_version` do `app` com `from app import
# current_version` — é um nome próprio no módulo routes.admin, então monkeypatchar
# `app_module.current_version` NÃO o afeta. Por isso estes dois testes usam a versão
# REAL do arquivo VERSION (via `app_module.current_version()`) em vez de fingir uma,
# e fixam um `latest` bem à frente dela (gap grande, nunca um patch único).

def test_update_page_shows_incomplete_warning_on_fallback(app_module, auth_client, monkeypatch):
monkeypatch.setattr(app_module, "_latest_release_tag_via_web", lambda: "v9.9.9")
monkeypatch.setattr(app_module, "latest_release_notes", lambda: "notas da ultima")
# Força o caminho de fallback: a busca do CHANGELOG cru falha.
monkeypatch.setattr(app_module, "_changelog_md",
lambda tag: (_ for _ in ()).throw(OSError("sem rede")))
html = auth_client.get("/admin/update").get_data(as_text=True)
assert "histórico completo" in html # aviso de notas possivelmente incompletas


def test_update_page_hides_warning_when_slicing_works(app_module, auth_client, monkeypatch):
cur = app_module.current_version()
changelog = (
"# Changelog\n\n"
"## [9.9.9] — 2026-07-28\n### Added\n- recurso novo\n\n"
f"## [{cur}] — 2026-01-01\n### Added\n- recurso antigo\n"
)
monkeypatch.setattr(app_module, "_latest_release_tag_via_web", lambda: "v9.9.9")
monkeypatch.setattr(app_module, "_changelog_md", lambda tag: changelog)
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
6 changes: 6 additions & 0 deletions translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
"The update downloads the latest published release and restarts the service. It may take 1–2 minutes.",
"Concluído! Recarregando…": "Done! Reloading…",
"Falha ao iniciar a atualização": "Failed to start the update",
"Não foi possível carregar o histórico completo; mostrando só as notas da última versão.":
"Could not load the full history; showing only the latest version's notes.",
"Ver tudo no GitHub": "See everything on GitHub",

# ── Backup / restore ─────────────────────────────────────────────────────
"Backup": "Backup",
Expand Down Expand Up @@ -530,6 +533,9 @@
"La actualización descarga la última versión publicada y reinicia el servicio. Puede tardar 1–2 minutos.",
"Concluído! Recarregando…": "¡Listo! Recargando…",
"Falha ao iniciar a atualização": "Error al iniciar la actualización",
"Não foi possível carregar o histórico completo; mostrando só as notas da última versão.":
"No se pudo cargar el historial completo; mostrando solo las notas de la última versión.",
"Ver tudo no GitHub": "Ver todo en GitHub",

# ── Backup / restore ─────────────────────────────────────────────────────
"Backup": "Copia de seguridad",
Expand Down