#!/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"¤t=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()