Files
hermes-scripts-backup/skills/productivity/briefing-quotidien/scripts/generate_briefing.py
T
2026-08-28 11:43:34 -04:00

973 lines
36 KiB
Python

#!/usr/bin/env python3
"""Generate a daily briefing markdown file and print the ObsiGate link.
Usage:
python3 generate_briefing.py # today's briefing
python3 generate_briefing.py 20260520 # specific date (YYYYMMDD)
"""
import json
import os
import subprocess
import sys
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.error import URLError
# ----------------------------------------------------------------
# Config
# ----------------------------------------------------------------
HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
GAPI = HERMES_HOME / "skills/productivity/google-workspace/scripts/google_api.py"
VAULT_BASE = Path("/NFS/OBSIDIAN_DOC/Obsidian_IT")
BRIEFING_DIR = VAULT_BASE / "05_Journal_Quotidien/5.1_Quotidien/HermesBerriPi/briefing-matin"
OBSIGATE_BASE = "https://og.dracodev.net/popout/IT"
# Beloeil, QC
LAT, LON = "45.5667", "-73.2"
# Calendar emoji map (by summary keyword)
CAL_EMOJI = {
"famille": "👪",
"travail": "💼",
"desjardins": "💼",
"finances": "💰",
"paie": "💰",
}
# Email action prefixes (from GitHub notification subjects)
ACTION_PREFIXES = {
"PR #": "🔀",
"Issue #": "💬",
"Merged #": "✅",
"pushé": "📌",
"commit": "📌",
}
# Weather WMO codes
WMO_EMOJI = {
0: "☀️", 1: "🌤️", 2: "⛅", 3: "☁️",
45: "🌫️", 48: "🌫️",
51: "🌧️", 53: "🌧️", 55: "🌧️",
61: "🌧️", 63: "🌧️", 65: "🌧️",
71: "❄️", 73: "❄️", 75: "❄️",
80: "🌧️", 81: "🌧️", 82: "🌧️",
85: "❄️", 86: "❄️",
95: "⛈️", 96: "⛈️", 99: "⛈️",
}
WMO_LABELS = {
0: "Ciel clair", 1: "Peu nuageux", 2: "Partiellement nuageux",
3: "Nuageux", 45: "Brouillard", 48: "Brouillard givrant",
51: "Bruine légère", 53: "Bruine modérée", 55: "Bruine dense",
61: "Pluie légère", 63: "Pluie modérée", 65: "Pluie forte",
71: "Neige légère", 73: "Neige modérée", 75: "Neige forte",
80: "Averses légères", 81: "Averses modérées", 82: "Averses violentes",
85: "Neige légère", 86: "Neige forte",
95: "Orage", 96: "Orage + grêle légère", 99: "Orage + grêle forte",
}
def shell(cmd, **kwargs):
"""Run a shell command and return stdout, exit_code."""
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
timeout=kwargs.get("timeout", 30)
)
return result.stdout.strip(), result.returncode
# ================================================================
# Section generators
# ================================================================
def get_date(date_id=None):
"""Return (date_id_yyyymmdd, day_fr, date_fr, jour_complet)."""
if date_id:
dt = datetime.strptime(date_id, "%Y%m%d")
else:
dt = datetime.now()
jours_fr = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
mois_fr = ["janvier", "février", "mars", "avril", "mai", "juin",
"juillet", "août", "septembre", "octobre", "novembre", "décembre"]
date_id_str = dt.strftime("%Y%m%d")
jour = jours_fr[dt.weekday()]
date_str = f"{jour} {dt.day} {mois_fr[dt.month-1]} {dt.year}"
return date_id_str, jour.capitalize(), date_str, f"{jour.capitalize()} {dt.day} {mois_fr[dt.month-1]} {dt.year}"
def get_citation():
"""Fetch random quote from zenquotes.io."""
try:
out, _ = shell("curl -s --max-time 5 'https://zenquotes.io/api/random'")
data = json.loads(out)
if data and isinstance(data, list):
q = data[0]["q"]
a = data[0]["a"]
return q, a
except Exception:
pass
# Fallback
return (
"La meilleure façon de prédire l'avenir est de le créer.",
"Peter Drucker"
)
def get_emails(max_count=5):
"""Fetch unread emails and format as markdown table rows."""
try:
out, _ = shell(
f"cd {GAPI.parent} && python3 {GAPI.name} gmail search 'is:unread' --max {max_count} 2>&1"
)
data = json.loads(out)
if not data:
return None, 0
rows = []
for i, email in enumerate(data, 1):
sender = email.get("from", "Inconnu")
# Clean sender: remove email address, keep name
if "<" in sender:
sender = sender.split("<")[0].strip().strip('"')
subject = email.get("subject", "(Sans objet)")
snippet = email.get("snippet", "")[:100]
# Action prefix
prefix = ""
subj_lower = subject.lower()
snippet_lower = snippet.lower()
if "pr #" in subj_lower:
if "merged" in snippet_lower:
prefix = "✅ "
else:
prefix = "🔀 "
elif "issue #" in subj_lower:
prefix = "💬 "
elif "push" in snippet_lower or "commit" in snippet_lower:
prefix = "📌 "
rows.append(f"| {i} | **{sender[:30]}** | `{subject[:50]}` | {prefix}{snippet[:80]} |")
return rows, len(data)
except Exception as e:
return None, 0
def get_agenda(date_id_str):
"""Fetch today's calendar events from ALL calendars."""
try:
# Use the dedicated script
script = HERMES_HOME / "skills/productivity/briefing-quotidien/scripts/agenda_all_calendars.py"
if script.exists():
out, _ = shell(f"python3 {script} {date_id_str} 2>&1")
else:
# Fallback: use google_api.py with primary + common calendars
out, _ = shell(
f"cd {GAPI.parent} && python3 {GAPI.name} calendar list --max 20 2>&1"
)
data = json.loads(out)
if not data:
return None, 0
# Filter today's events (date_id_str = YYYYMMDD)
today_events = []
for ev in data:
start = ev.get("start", "")
if date_id_str in start.replace("-", ""):
today_events.append(ev)
if not today_events:
return None, 0
rows = []
for ev in today_events:
start = ev.get("start", "")
summary = ev.get("summary", "(Sans titre)")
# Format time
if "T" in start:
time_part = start.split("T")[1][:5]
time_str = f"🕐 **{time_part}**"
else:
time_str = "📆 Journée"
# Determine category emoji
summary_lower = summary.lower()
cat_emoji = "📅" # default
for keyword, emoji in CAL_EMOJI.items():
if keyword in summary_lower:
cat_emoji = emoji
break
# Also check calendar name if available
cal_name = ev.get("calendar", "").lower()
for keyword, emoji in CAL_EMOJI.items():
if keyword in cal_name:
cat_emoji = emoji
break
rows.append(f"| {time_str} | {summary} | {cat_emoji} |")
return rows, len(today_events)
except Exception as e:
# If agenda_all_calendars.py doesn't exist, try direct approach
return None, 0
def get_meteo():
"""Fetch weather for Beloeil from Open-Meteo."""
try:
out, _ = shell(
f"curl -s --max-time 5 "
f"'https://api.open-meteo.com/v1/forecast?"
f"latitude={LAT}&longitude={LON}"
f"&current=temperature_2m,relative_humidity_2m,apparent_temperature,wind_speed_10m,weather_code"
f"&daily=temperature_2m_max,temperature_2m_min"
f"&timezone=America%2FToronto&forecast_days=3'"
)
data = json.loads(out)
current = data.get("current", {})
daily = data.get("daily", {})
wmo = current.get("weather_code", 0)
emoji = WMO_EMOJI.get(wmo, "❓")
label = WMO_LABELS.get(wmo, "Inconnu")
temp = current.get("temperature_2m", "?")
feels = current.get("apparent_temperature", "?")
wind = current.get("wind_speed_10m", "?")
hum = current.get("relative_humidity_2m", "?")
# Current table
current_row = f"| **Aujourd'hui** | {emoji} {label} | **{temp}°C** | {feels}°C | {wind} km/h | {hum}% |"
# Forecast table
jours = ["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."]
forecast_rows = []
for i in range(1, min(3, len(daily.get("time", [])))):
date_str = daily["time"][i]
dt = datetime.strptime(date_str, "%Y-%m-%d")
jour = jours[dt.weekday()]
tmax = daily["temperature_2m_max"][i]
tmin = daily["temperature_2m_min"][i]
label_jour = "Demain" if i == 1 else "Après-demain"
forecast_rows.append(f"| 📅 **{label_jour}** ({jour}) | {tmin}°C | {tmax}°C |")
return current_row, forecast_rows
except Exception as e:
return None, None
def get_system():
"""Check Hermes services and system resources."""
# Services
services = ["hermes-gateway", "hermes-dashboard", "hermes-workspace"]
svc_rows = []
svc_ok = 0
for svc in services:
out, rc = shell(f"systemctl --user is-active {svc} 2>/dev/null")
if out == "active":
svc_rows.append(f"| {svc.replace('hermes-', '').capitalize()} | 🟢 |")
svc_ok += 1
else:
svc_rows.append(f"| {svc.replace('hermes-', '').capitalize()} | 🔴 |")
svc_status = f"🟢 {svc_ok}/3 en ligne" if svc_ok == 3 else f"⚠️ {svc_ok}/3 en ligne"
# Resources
try:
# CPU
out, _ = shell("top -bn1 | grep 'Cpu(s)' | awk '{print $2}'")
cpu = out.replace("%", "").strip() if out else "?"
cpu = f"{cpu}% `us`" if cpu and cpu != "?" else "?"
# RAM
out, _ = shell("free -m | awk 'NR==2{printf \"%.0f\", $3*100/$2}'")
ram = f"{out}%" if out else "?"
# Disk
out, _ = shell("df -h / | awk 'NR==2{print $5}' | sed 's/%//'")
disk = f"{out}%" if out else "?"
# Uptime
out, _ = shell("uptime -p | sed 's/up //'")
uptime = out if out else "?"
except Exception:
cpu = ram = disk = uptime = "?"
return svc_status, svc_rows, cpu, ram, disk, uptime
def get_docker_services():
"""List running Docker containers with status and uptime."""
try:
out, rc = shell("docker ps --format '{{.Names}}\t{{.Status}}\t{{.Image}}' 2>/dev/null")
if rc != 0 or not out:
return None, 0
containers = []
for line in out.split("\n"):
parts = line.split("\t")
if len(parts) >= 2:
name = parts[0]
status = parts[1]
# Parse status: "Up 2 hours" or "Up 3 days (healthy)"
is_healthy = "(healthy)" in status or "(unhealthy)" not in status
is_up = status.startswith("Up")
emoji = "🟢" if (is_up and is_healthy) else ("🟡" if is_up else "🔴")
containers.append((name, emoji, status))
return containers, len(containers)
except Exception:
return None, 0
def get_docker_stats():
"""Get resource usage stats for Docker containers."""
try:
out, rc = shell(
"docker stats --no-stream --format "
"'{{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}' 2>/dev/null"
)
if rc != 0 or not out:
return None
stats = []
for line in out.split("\n"):
parts = line.split("\t")
if len(parts) >= 4:
# Clean up: remove spaces in percentages, trim
name = parts[0]
cpu = parts[1].strip()
mem_pct = parts[2].strip()
# NetIO: "1.2MB / 3.4MB" → take IN only
net = parts[4].strip() if len(parts) >= 5 else "—"
net_in = net.split(" / ")[0] if " / " in net else net
stats.append((name, cpu, mem_pct, net_in))
return stats
except Exception:
return None
def get_honcho_stats():
"""Query Honcho API for workspace statistics."""
try:
import urllib.request
BASE = "http://localhost:8000/v3"
WS = "hermes"
def post_json(path, data=None):
body = json.dumps(data or {}).encode()
req = urllib.request.Request(f"{BASE}{path}", data=body, method="POST",
headers={"Content-Type": "application/json"})
resp = urllib.request.urlopen(req, timeout=3)
return json.loads(resp.read())
# Health check
try:
req = urllib.request.urlopen("http://localhost:8000/health", timeout=3)
health = json.loads(req.read())
api_ok = health.get("status") == "ok"
except Exception:
api_ok = False
# Peers
try:
data = post_json(f"/workspaces/{WS}/peers/list")
items = data.get("items", data)
nb_peers = len(items) if isinstance(items, list) else "?"
except Exception:
nb_peers = "?"
# Sessions
try:
data = post_json(f"/workspaces/{WS}/sessions/list")
items = data.get("items", data)
nb_sessions = len(items) if isinstance(items, list) else "?"
except Exception:
nb_sessions = "?"
# Conclusions (observations)
try:
data = post_json(f"/workspaces/{WS}/conclusions/list")
items = data.get("items", data)
nb_conclusions = len(items) if isinstance(items, list) else "?"
except Exception:
nb_conclusions = "?"
return {
"api": api_ok,
"peers": nb_peers,
"sessions": nb_sessions,
"conclusions": nb_conclusions,
}
except Exception:
return None
def get_priorities():
"""Read priorities file if it exists."""
priorities_file = Path.home() / ".hermes" / "priorities.txt"
if priorities_file.exists():
return priorities_file.read_text().strip()
return None
def get_news_freshrss():
"""Collect news from FreshRSS via Fever API, grouped by FreshRSS categories.
Returns dict: {category_name: [(title, url, source, cat_emoji), ...]}
Falls back to None if FreshRSS is unavailable."""
try:
FRESHRSS_API = "http://localhost:8088/api/fever.php"
API_KEY = "c2cc2566601a7b5c6f28fcc8268786d4" # md5("admin:freshrss2026")
# 1. Get all unread item IDs
data = f"api&email=admin&api_key={API_KEY}&unread_item_ids".encode()
req = Request(FRESHRSS_API, data=data)
with urlopen(req, timeout=10) as resp:
result = json.loads(resp.read())
unread_ids = result.get("unread_item_ids", "")
if not unread_ids:
return None
all_ids = unread_ids.split(",")
# Sample evenly across the full ID range to hit all feeds/categories.
# FreshRSS returns IDs sorted by date (newest first), so taking only
# the first N would capture only the most active feed (usually International).
# By sampling every Nth ID we span the full time range and get all categories.
MAX_SAMPLE = 200
step = max(1, len(all_ids) // MAX_SAMPLE)
sampled_ids = all_ids[::step][:MAX_SAMPLE]
# 2. Get feeds and groups first (to know which feeds exist)
data = f"api&email=admin&api_key={API_KEY}&feeds&groups".encode()
req = Request(FRESHRSS_API, data=data)
with urlopen(req, timeout=10) as resp:
result = json.loads(resp.read())
feeds = {f["id"]: f for f in result.get("feeds", [])}
groups = {g["id"]: g["title"] for g in result.get("groups", [])}
feeds_groups = {}
for fg in result.get("feeds_groups", []):
for fid in fg["feed_ids"].split(","):
fid_int = int(fid) if fid.isdigit() else fid
feeds_groups[fid_int] = fg["group_id"]
# 3. Get items for sampled IDs
data = f"api&email=admin&api_key={API_KEY}&items&with_ids={','.join(sampled_ids)}".encode()
req = Request(FRESHRSS_API, data=data)
with urlopen(req, timeout=10) as resp:
result = json.loads(resp.read())
items = result.get("items", [])
# Category emoji mapping
CAT_EMOJI = {
"International": "🌍",
"Regional": "🇨🇦",
"Sciences": "🔬",
"Technologies": "💻",
"Développement": "⚙️",
"Intelligence Artificielle": "🤖",
}
MAX_PER_CATEGORY = 5 # Keep briefing concise but diverse
# Group items by category
cat_items = {}
for item in items:
fid = item.get("feed_id")
if fid not in feeds:
continue
feed = feeds[fid]
gid = feeds_groups.get(fid, 1)
cat_name = groups.get(gid, "Sans catégorie")
if cat_name == "Sans catégorie":
continue # Skip uncategorized items
emoji = CAT_EMOJI.get(cat_name, "📰")
if cat_name not in cat_items:
cat_items[cat_name] = []
if len(cat_items[cat_name]) >= MAX_PER_CATEGORY:
continue # Limit per category
cat_items[cat_name].append((
item.get("title", "")[:120],
item.get("html_url", item.get("url", "")),
feed.get("title", ""),
emoji,
))
return cat_items if cat_items else None
except Exception:
return None
def get_news_rss():
"""Collect tech/AI news from RSS feeds (fallback when FreshRSS is unavailable).
Returns list of (title, url, source, category)."""
FEEDS = [
("https://feeds.arstechnica.com/arstechnica/index", "Ars Technica", "🔬"),
("https://www.theverge.com/rss/index.xml", "The Verge", "💻"),
("https://news.mit.edu/topic/mitartificial-intelligence2-rss.xml", "MIT News", "🔬"),
("https://techcrunch.com/feed/", "TechCrunch", "🏛️"),
("https://www.wired.com/feed/category/security/latest/rss", "Wired Security", "🛡️"),
]
items = []
for url, source, default_cat in FEEDS:
try:
req = Request(url, headers={"User-Agent": "HermesBerryPi/1.0"})
with urlopen(req, timeout=10) as resp:
tree = ET.fromstring(resp.read())
# RSS 2.0
for item_elem in tree.iter("item"):
title = item_elem.find("title")
link = item_elem.find("link")
title_text = title.text.strip() if title is not None and title.text else ""
link_text = link.text.strip() if link is not None and link.text else ""
if title_text and link_text:
# Catégorie basée sur mots-clés dans le titre
title_lower = title_text.lower()
if any(w in title_lower for w in ["ai ", "gpt", "llm", "model", "machine learning", "deep learning", "neural"]):
cat = "🔬"
elif any(w in title_lower for w in ["security", "hack", "vuln", "cyber", "breach", "ransom"]):
cat = "🛡️"
elif any(w in title_lower for w in ["lawsuit", "regulation", "eu ", "ftc", "privacy", "copyright"]):
cat = "🏛️"
else:
cat = default_cat
items.append((title_text[:120], link_text, source, cat))
# Atom feed
if not items or all(i[2] != source for i in items[-5:]):
for entry in tree.iter("{http://www.w3.org/2005/Atom}entry"):
title = entry.find("{http://www.w3.org/2005/Atom}title")
link = entry.find("{http://www.w3.org/2005/Atom}link")
title_text = title.text.strip() if title is not None and title.text else ""
link_text = link.get("href", "").strip() if link is not None else ""
if title_text and link_text:
title_lower = title_text.lower()
if any(w in title_lower for w in ["ai ", "gpt", "llm", "model"]):
cat = "🔬"
elif any(w in title_lower for w in ["security", "hack", "vuln"]):
cat = "🛡️"
else:
cat = default_cat
items.append((title_text[:120], link_text, source, cat))
except Exception:
continue
# Dédupliquer par titre (similarité simple)
seen = set()
unique = []
for title, url, source, cat in items:
key = title[:50].lower()
if key not in seen:
seen.add(key)
unique.append((title, url, source, cat))
return unique[:10] # Max 10 articles
def get_honcho_advanced():
"""Query Honcho API for detailed workspace statistics."""
try:
import urllib.request as ur
BASE = "http://localhost:8000/v3"
WS = "hermes"
def post_json(path, data=None):
body = json.dumps(data or {}).encode()
req = ur.Request(f"{BASE}{path}", data=body, method="POST",
headers={"Content-Type": "application/json"})
resp = ur.urlopen(req, timeout=5)
return json.loads(resp.read())
stats = {}
# Health
try:
req = ur.urlopen("http://localhost:8000/health", timeout=3)
health = json.loads(req.read())
stats["api"] = health.get("status") == "ok"
ver = health.get("version", "")
if not ver:
# Try /v3/workspaces/{WS} for version info
try:
ws_data = post_json(f"/workspaces/{WS}")
ver = ws_data.get("version", "")
except Exception:
pass
stats["version"] = ver if ver else "?"
except Exception:
stats["api"] = False
stats["version"] = "?"
# Peers
try:
data = post_json(f"/workspaces/{WS}/peers/list")
items = data.get("items", data)
stats["peers"] = len(items) if isinstance(items, list) else "?"
except Exception:
stats["peers"] = "?"
# Sessions
try:
data = post_json(f"/workspaces/{WS}/sessions/list")
items = data.get("items", data)
stats["sessions"] = len(items) if isinstance(items, list) else "?"
# Sessions actives (7 derniers jours)
cutoff = (datetime.now() - timedelta(days=7)).isoformat()
recent = 0
if isinstance(items, list):
for s in items:
created = s.get("created_at", s.get("created", ""))
if created and created > cutoff:
recent += 1
stats["sessions_recent"] = recent
except Exception:
stats["sessions"] = "?"
stats["sessions_recent"] = "?"
# Conclusions
try:
data = post_json(f"/workspaces/{WS}/conclusions/list")
items = data.get("items", data)
stats["conclusions"] = len(items) if isinstance(items, list) else "?"
except Exception:
stats["conclusions"] = "?"
# DB size
try:
out, _ = shell("du -sh /home/bruno/workspace/honcho/ 2>/dev/null | cut -f1")
stats["db_size"] = out if out else "?"
except Exception:
stats["db_size"] = "?"
return stats
except Exception:
return None
def get_system_advanced():
"""Get advanced system metrics (temperature, load, I/O)."""
metrics = {}
# Température CPU (Raspberry Pi)
try:
out, rc = shell("vcgencmd measure_temp 2>/dev/null")
if rc == 0 and out:
metrics["cpu_temp"] = out.replace("temp=", "").replace("'C", "°C")
else:
metrics["cpu_temp"] = "?"
except Exception:
metrics["cpu_temp"] = "?"
# Load average
try:
out, rc = shell("cat /proc/loadavg 2>/dev/null | awk '{print $1, $2, $3}'")
metrics["load"] = out if rc == 0 and out else "?"
except Exception:
metrics["load"] = "?"
# Disk I/O
try:
out, rc = shell("iostat -d sda 2>/dev/null | awk 'NR==4{print $3, $4}' || iostat -d mmcblk0 2>/dev/null | awk 'NR==4{print $3, $4}'")
if rc == 0 and out:
metrics["disk_io"] = out
else:
# Fallback: /proc/diskstats
out2, _ = shell("awk '{if($3==\"mmcblk0\"||$3==\"sda\") printf \"%.1f MB/s\", $7*512/1024/1024}' /proc/diskstats 2>/dev/null")
metrics["disk_io"] = out2 if out2 else "?"
except Exception:
metrics["disk_io"] = "?"
# Swap
try:
out, rc = shell("free -m | awk 'NR==3{printf \"%.0f/%.0fMB\", $3, $2}'")
metrics["swap"] = out if rc == 0 and out else "?"
except Exception:
metrics["swap"] = "?"
# Uptime
try:
out, rc = shell("uptime -p | sed 's/up //'")
metrics["uptime"] = out if rc == 0 and out else "?"
except Exception:
metrics["uptime"] = "?"
return metrics
def get_docker_advanced():
"""Get detailed Docker container info with ports and health."""
try:
out, rc = shell("docker ps --format '{{.Names}}\\t{{.Status}}\\t{{.Image}}\\t{{.Ports}}' 2>/dev/null")
if rc != 0 or not out:
return None, 0
containers = []
for line in out.split("\n"):
parts = line.split("\t")
if len(parts) >= 2:
name = parts[0]
status = parts[1]
image = parts[2] if len(parts) > 2 else "?"
ports = parts[3] if len(parts) > 3 else "—"
# Health status
is_healthy = "(healthy)" in status
is_unhealthy = "(unhealthy)" in status
is_up = status.startswith("Up")
if is_up and is_healthy:
emoji = "🟢"
elif is_up and is_unhealthy:
emoji = "🟡"
elif is_up:
emoji = "🟢" # No healthcheck = assume OK
else:
emoji = "🔴"
# Clean ports display
if ports and ports != "—" and len(ports) > 20:
# Take first port mapping only
ports = ports.split(",")[0].strip()
containers.append((name, emoji, status, image, ports))
return containers, len(containers)
except Exception:
return None, 0
def build_markdown(date_id_str, jour, date_complete):
"""Build the complete briefing markdown."""
parts = []
# ---- Header ----
parts.append(f"# 📅 Briefing Matinal\n")
parts.append(f"> **{date_complete}** · 🕖 07:00 · `America/Toronto`\n")
parts.append("---\n")
# ---- Citation ----
quote, author = get_citation()
parts.append(f"# 💡 Motivation du jour\n")
parts.append(f"> *\"{quote}\"*\n")
parts.append(f"> — **{author}**\n")
parts.append("---\n")
# ---- Emails ----
parts.append(f"# 📧 Emails récents\n")
emails, nb_emails = get_emails()
if emails:
parts.append(f"| # | Expéditeur | Sujet | Résumé |")
parts.append(f"|:-:|:-----------|:------|:-------|")
parts.extend(emails)
parts.append("")
else:
parts.append("📭 Aucun email non lu.\n")
parts.append("---\n")
# ---- Agenda ----
parts.append(f"# 📅 Agenda du jour\n")
agenda, nb_events = get_agenda(date_id_str)
if agenda:
parts.append(f"| Heure | Événement | Catégorie |")
parts.append(f"|:-----:|:----------|:---------:|")
parts.extend(agenda)
parts.append("")
else:
parts.append("📅 Aucun événement aujourd'hui.\n")
parts.append("---\n")
# ---- Météo ----
parts.append(f"# 🌤️ Météo — Beloeil\n")
current_row, forecast_rows = get_meteo()
if current_row:
parts.append(f"| | Condition | Temp. | Ressenti | Vent | Humidité |")
parts.append(f"|:-:|:---------:|:-----:|:--------:|:----:|:--------:|")
parts.append(current_row)
parts.append("")
if forecast_rows:
parts.append(f"| | 🌡️ Min | 🌡️ Max |")
parts.append(f"|:-:|:------:|:------:|")
parts.extend(forecast_rows)
parts.append("")
else:
parts.append("🌤️ Météo non disponible.\n")
parts.append("---\n")
# ---- System ----
parts.append(f"# 🖥️ HermesBerryPi\n")
svc_status, svc_rows, cpu, ram, disk, uptime = get_system()
sys_adv = get_system_advanced()
parts.append(f"Services — {svc_status}\n")
parts.append(f"| Service | Statut |")
parts.append(f"|:--------|:------:|")
parts.extend(svc_rows)
parts.append("")
parts.append(f"Ressources système\n")
parts.append(f"| CPU | RAM | Disque | Swap | Uptime |")
parts.append(f"|:---:|:---:|:------:|:----:|:------:|")
parts.append(f"| {cpu} | {ram} | {disk} | {sys_adv.get('swap', '?')} | {sys_adv.get('uptime', uptime)} |")
parts.append("")
parts.append(f"| 🌡️ Temp. CPU | 📊 Load (1/5/15) | 💾 I/O Disque |")
parts.append(f"|:-------------:|:-----------------:|:-------------:|")
parts.append(f"| {sys_adv.get('cpu_temp', '?')} | {sys_adv.get('load', '?')} | {sys_adv.get('disk_io', '?')} |")
parts.append("")
parts.append("---\n")
# ---- Docker ----
parts.append(f"# 🐳 Docker\n")
containers, nb_containers = get_docker_advanced()
if containers:
stats = get_docker_stats() or []
stat_map = {s[0]: s for s in stats}
up_count = sum(1 for _, e, _, _, _ in containers if e == "🟢")
parts.append(f"Services — 🟢 {up_count}/{nb_containers} conteneurs actifs\n")
parts.append(f"| Conteneur | Image | Statut | CPU | RAM | Réseau IN | Ports |")
parts.append(f"|:----------|:------|:------:|:---:|:---:|:----------:|:------|")
for name, emoji, status, image, ports in containers:
cpu = mem = net = "—"
if name in stat_map:
_, cpu, mem, net = stat_map[name]
# Shorten image name
img_short = image.split("/")[-1] if "/" in image else image
if ":" in img_short:
img_short = img_short.split(":")[0]
parts.append(f"| {name} | {img_short} | {emoji} {status} | {cpu} | {mem} | {net} | {ports} |")
parts.append("")
else:
parts.append("🐳 Docker non disponible.\n")
parts.append("---\n")
# ---- Honcho ----
parts.append(f"# 🧠 Honcho (mémoire IA)\n")
honcho = get_honcho_advanced()
if honcho:
api_emoji = "🟢" if honcho["api"] else "🔴"
parts.append(f"| Métrique | Valeur |")
parts.append(f"|:---------|:------:|")
parts.append(f"| API | {api_emoji} {'OK' if honcho['api'] else 'HORS-LIGNE'} |")
parts.append(f"| Peers | {honcho['peers']} |")
parts.append(f"| Sessions (total) | {honcho['sessions']} |")
parts.append(f"| Sessions (7j) | {honcho.get('sessions_recent', '?')} |")
parts.append(f"| Conclusions | {honcho['conclusions']} |")
parts.append(f"| Stockage DB | {honcho.get('db_size', '?')} |")
parts.append("")
else:
parts.append("🧠 Honcho non disponible.\n")
parts.append("---\n")
# ---- Priorities ----
parts.append(f"# 🎯 Priorités du jour\n")
priorities = get_priorities()
if priorities:
parts.append(priorities)
else:
parts.append("")
parts.append("⚠️ Aucune priorité définie — pensez à planifier votre journée !")
parts.append("")
parts.append("---\n")
# ---- Actualités RSS (FreshRSS prioritaire, fallback flux directs) ----
parts.append(f"# 📡 Actualités RSS\n")
parts.append("")
# Try FreshRSS first
freshrss_news = get_news_freshrss()
if freshrss_news:
total_articles = sum(len(v) for v in freshrss_news.values())
for cat_name, articles in freshrss_news.items():
if not articles:
continue
emoji = articles[0][3] if articles else "📰"
parts.append(f"{emoji} {cat_name}\n")
parts.append(f"| | Titre | Source |")
parts.append(f"|:-:|:------|:------:|")
for title, url, source, _ in articles[:4]:
parts.append(f"| 📰 | [**{title}**]({url}) | {source} |")
parts.append("")
parts.append(f"_📡 {total_articles} articles via FreshRSS ({len(freshrss_news)} catégories)_")
else:
# Fallback to direct RSS feeds
news = get_news_rss()
if news:
# Grouper par catégorie
categories = {}
for title, url, source, cat in news:
if cat not in categories:
categories[cat] = []
categories[cat].append((title, url, source))
cat_labels = {"🔬": "Recherche & Produits", "🏛️": "Juridique & Business", "🛡️": "Sécurité & Alignement", "💻": "Tech & Industrie"}
for cat, cat_label in cat_labels.items():
if cat in categories:
parts.append(f"{cat} {cat_label}\n")
parts.append(f"| | Titre | Source |")
parts.append(f"|:-:|:------|:------:|")
for title, url, source in categories[cat][:4]:
parts.append(f"| 📰 | [**{title}**]({url}) | {source} |")
parts.append("")
parts.append(f"_📡 {len(news)} articles collectés via flux RSS directs_")
else:
parts.append("📡 Aucune actualité disponible (FreshRSS et flux RSS inaccessibles).\n")
parts.append("")
parts.append("---\n")
# ---- Footer ----
parts.append("Bonne journée! ☀️\n")
return "\n".join(parts), nb_emails, nb_events
def main():
date_id_str = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y%m%d")
date_id_str, jour, _, date_complete = get_date(date_id_str)
year = date_id_str[:4]
month = date_id_str[4:6]
# Build markdown
md, nb_emails, nb_events = build_markdown(date_id_str, jour, date_complete)
# Save
target_dir = BRIEFING_DIR / year / month
target_dir.mkdir(parents=True, exist_ok=True)
filepath = target_dir / f"briefing_{date_id_str}.md"
filepath.write_text(md)
# ObsiGate link
rel_path = f"05_Journal_Quotidien/5.1_Quotidien/HermesBerriPi/briefing-matin/{year}/{month}/briefing_{date_id_str}.md"
encoded = rel_path.replace("/", "%2F")
link = f"{OBSIGATE_BASE}/{encoded}"
# Output JSON for cron
result = {
"date_id": date_id_str,
"file": str(filepath),
"link": link,
"emails": nb_emails,
"events": nb_events,
}
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()