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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.36.1
1.36.2
19 changes: 12 additions & 7 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/test_update_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down