feat(palier-3): recherche globale, mes issues, briefing ntfy, MCP lecture
- client: search_issues (/repos/issues/search, type=issues, assigned_by), delete_label + forget_label_cache; models: kv_store - /search groupe par projet avec colonne Kanban; /my = issues ouvertes assignées à l'utilisateur du jeton (get_user) - briefing: _briefing_data (focus, âgées, milestones <= 7 j, activité journal), page /briefing, /api/briefing, envoi ntfy manuel + planificateur lifespan à KANBANTE_BRIEFING_TIME (kv briefing_last_sent, pas de doublon); app/briefing.py = format texte + envoi ntfy (en-têtes ASCII) - MCP: POST /mcp JSON-RPC (initialize, tools/list, tools/call, ping, notifications 202), 7 outils en lecture seule, GET /mcp de découverte; erreurs -32601/-32700, isError pour les outils - focus refactoré en _collect_focus_groups réutilisé par le briefing et l'outil MCP get_focus
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user