diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa6f83..e1a85fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ Versioning follows [SemVer](https://semver.org/): **MAJOR.MINOR.PATCH** --- +## [1.36.2] — 2026-06-11 + +### Fixed +- **Card "Ver novidades" some quando a API do GitHub falha (fica só o link).** A + página de atualização busca as notas da release pela REST API do GitHub + (`/releases/latest`), que é fail-open. Numa falha pontual (rate limit de 60/h por + IP — não há token no servidor — ou timeout) `latest_release_notes()` devolvia `''` + e o template caía no link simples do GitHub em vez do card expansível. Agora a + busca **reusa a última nota obtida com sucesso** em vez de zerar (o card só vira + link se **nunca** conseguimos notas) e o timeout subiu de 4s para 8s. + ## [1.36.1] — 2026-06-11 Responsividade no celular. A base já era mobile-first (Bootstrap 5.3, navbar diff --git a/VERSION b/VERSION index f107550..c6a567b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.36.1 +1.36.2 diff --git a/app.py b/app.py index 8004753..801a74c 100644 --- a/app.py +++ b/app.py @@ -406,11 +406,13 @@ def cached_latest_tag(): def latest_release_notes(): """Notas (Markdown) da última release, via REST API — chamada SÓ na página de - update. Tolerante a falha (rate limit / sem rede): devolve '' e a página mostra - o update sem as notas. Cacheada pela tag corrente.""" + update. Tolerante a falha (rate limit 60/h por IP / sem rede / timeout): em vez + de zerar, **reusa a última nota obtida com sucesso**, para o card "Ver novidades" + não cair pro link do GitHub por uma falha pontual da API. Só devolve '' (→ link) + se NUNCA conseguimos notas. Cacheada pela tag corrente.""" tag = _release_cache.get("tag") if not tag: - return "" + return _notes_cache["notes"] if _notes_cache["tag"] == tag and _notes_cache["notes"]: return _notes_cache["notes"] try: @@ -420,14 +422,17 @@ def latest_release_notes(): GITHUB_RELEASES_API, headers={"User-Agent": "spool-control", "Accept": "application/vnd.github+json"}, ) - with _safe_opener.open(req, timeout=4) as resp: + with _safe_opener.open(req, timeout=8) as resp: data = json.loads(resp.read().decode()) notes = (data.get("body") or "").strip() - _notes_cache.update(tag=tag, notes=notes) - return notes + if notes: + _notes_cache.update(tag=tag, notes=notes) + return notes except Exception: log.info("github_release.notes_unavailable", exc_info=True) - return "" + # fail-open: mantém a última nota boa (mesmo que de outra tag) p/ o card sobreviver + # a uma falha pontual; '' só quando nunca obtivemos notas → cai pro link. + return _notes_cache["notes"] # Subconjunto de Markdown usado nas release notes → HTML seguro, sem dependência diff --git a/tests/test_update_check.py b/tests/test_update_check.py index e5905ad..1089785 100644 --- a/tests/test_update_check.py +++ b/tests/test_update_check.py @@ -69,6 +69,28 @@ def test_update_page_checks_via_web(app_module, monkeypatch): assert app_module._release_cache["tag"] == "9.9.9" +# ── Notas da release: resiliência (card "Ver novidades" sobrevive a falha) ──── + +def _boom(*a, **k): + raise OSError("github fora do ar") + + +def test_release_notes_reuses_last_good_on_failure(app_module, monkeypatch): + """Se a busca falhar mas já tivermos notas boas em cache, reusa (card continua).""" + app_module._release_cache.update(tag="1.36.1", ts=0.0, ok=True) + app_module._notes_cache.update(tag="1.36.0", notes="notas antigas ok") + monkeypatch.setattr(app_module._safe_opener, "open", _boom) + assert app_module.latest_release_notes() == "notas antigas ok" + + +def test_release_notes_empty_when_never_fetched(app_module, monkeypatch): + """Sem nenhuma nota boa em cache, falha → '' (a página cai pro link do GitHub).""" + app_module._release_cache.update(tag="1.36.1", ts=0.0, ok=True) + app_module._notes_cache.update(tag=None, notes="") + monkeypatch.setattr(app_module._safe_opener, "open", _boom) + assert app_module.latest_release_notes() == "" + + # ── Status do update (lido pela /admin/update p/ mostrar falha) ─────────────── def test_update_status_idle_when_no_file(app_module):