"""Daily briefing: formatting and ntfy delivery. The data itself is gathered in app.main (it needs the board helpers); this module only turns it into a readable text and pushes it to an ntfy topic. Kept dependency-light so it is unit-testable alone. """ from __future__ import annotations import httpx from app.config import Settings def ntfy_configured(settings: Settings) -> bool: return bool(settings.ntfy_url and settings.ntfy_topic) def format_briefing_text(data: dict) -> str: """Plain-text briefing (ntfy body, also shown on /briefing).""" lines: list[str] = [f"📋 Kanbante — briefing du {data['date']}"] activity = data["activity_24h"] lines.append( f"\n⚡ Dernières 24 h : {activity['closed']} fermée(s), " f"{activity['created']} créée(s), {activity['moved']} déplacement(s)." ) if activity.get("avg_cycle_days") is not None: lines.append(f"⏱️ Temps de cycle moyen (30 j) : {activity['avg_cycle_days']} j.") lines.append(f"\n🎯 En cours / en révision : {data['focus_total']} carte(s)") for group in data["focus_groups"][:6]: lines.append(f" • {group['full_name']} : {group['count']}") for title in group["titles"][:3]: lines.append(f" – {title}") if data["aging"]: lines.append("\n⏳ Cartes qui vieillissent :") for card in data["aging"][:8]: lines.append( f" • {card['repo']} #{card['number']} « {card['title']} » " f"— {card['days']} j en {card['column_label']}" ) if data["milestones_due"]: lines.append("\n📅 Milestones proches :") for ms in data["milestones_due"][:5]: lines.append( f" • {ms['repo']} « {ms['title']} » — échéance " f"{ms['due_on']} ({ms['open']} ouverte(s))" ) if data["focus_total"] == 0 and not data["aging"]: lines.append("\n🎉 Rien d'urgent : aucun encours, aucune carte vieillissante.") return "\n".join(lines) async def send_ntfy(settings: Settings, text: str) -> bool: """POST the briefing to the configured ntfy topic. True on 2xx.""" if not ntfy_configured(settings): return False url = f"{settings.ntfy_url.rstrip('/')}/{settings.ntfy_topic}" headers = { # ntfy headers must stay ASCII; the body carries the accents. "Title": "Kanbante - briefing du jour", "Tags": "clipboard,chart_with_upwards_trend", } if settings.ntfy_token: headers["Authorization"] = f"Bearer {settings.ntfy_token}" async with httpx.AsyncClient(timeout=15.0) as client: response = await client.post(url, content=text.encode("utf-8"), headers=headers) return response.status_code < 300