Files
blackroad/bin/slack-notify
Alexa Amundson 78fbe80f2a Initial monorepo — everything BlackRoad in one place
bin/       230 CLI tools (ask-*, br-*, agent-*, roadid, carpool)
scripts/   99 automation scripts
fleet/     Node configs and deployment
workers/   Cloudflare Worker sources (roadpay, road-search, squad webhooks)
roadc/     RoadC programming language
roadnet/   Mesh network (5 APs, WireGuard)
operator/  Memory system scripts
config/    System configs
dotfiles/  Shell configs
docs/      Documentation

BlackRoad OS — Pave Tomorrow.

RoadChain-SHA2048: d1a24f55318d338b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
2026-03-14 17:08:41 -05:00

237 lines
8.6 KiB
Bash
Executable File

#!/bin/bash
# Post daily KPI report to Slack (blackroadosinc.slack.com)
# Enhanced: trend deltas, fleet health, git-agent status, alert severity
#
# Requires SLACK_WEBHOOK_URL env var or ~/.blackroad/slack-webhook.env
# Optional: SLACK_ALERTS_WEBHOOK_URL for #alerts channel
source "$(dirname "$0")/../lib/common.sh"
source "$(dirname "$0")/../lib/slack.sh"
slack_load
if ! slack_ready; then
err "Slack not configured. Run: bash scripts/setup-slack.sh"
exit 1
fi
DAILY=$(today_file)
if [ ! -f "$DAILY" ]; then
err "No daily data for $TODAY. Run: bash collectors/collect-all.sh"
exit 1
fi
YESTERDAY=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d '1 day ago' +%Y-%m-%d)
YESTERDAY_FILE="$DATA_DIR/daily/${YESTERDAY}.json"
GIT_AGENT_LOG="$HOME/.blackroad/logs/git-agent.log"
export DAILY YESTERDAY_FILE GIT_AGENT_LOG
# ─── Build Slack payload ─────────────────────────────────────────────
payload=$(python3 << 'PYEOF'
import json, os, glob
daily_file = os.environ.get('DAILY', '')
yesterday_file = os.environ.get('YESTERDAY_FILE', '')
git_log = os.environ.get('GIT_AGENT_LOG', '')
with open(daily_file) as f:
data = json.load(f)
s = data['summary']
# Yesterday's data for deltas
ys = {}
if yesterday_file and os.path.exists(yesterday_file):
with open(yesterday_file) as f:
ys = json.load(f).get('summary', {})
def delta(key, invert=False):
"""Show delta from yesterday: +N or -N"""
curr = s.get(key, 0)
prev = ys.get(key, 0)
if not prev or not isinstance(curr, (int, float)):
return ''
diff = curr - prev
if diff == 0:
return ''
sign = '+' if diff > 0 else ''
emoji = ''
if invert: # lower is better (failed_units, throttled)
emoji = ' :small_red_triangle:' if diff > 0 else ' :small_red_triangle_down:'
else:
emoji = ' :chart_with_upwards_trend:' if diff > 0 else ' :chart_with_downwards_trend:'
return f" ({sign}{diff}{emoji})"
def fmt(n):
if isinstance(n, float):
return f"{n:,.1f}"
if isinstance(n, int) and n >= 1000:
return f"{n:,}"
return str(n)
# Fleet status emoji
fleet_online = s.get('fleet_online', 0)
fleet_total = s.get('fleet_total', 4)
fleet_emoji = ':large_green_circle:' if fleet_online == fleet_total else ':red_circle:' if fleet_online <= 1 else ':large_yellow_circle:'
# Autonomy score bar
score = s.get('autonomy_score', 0)
filled = score // 10
score_bar = ':black_large_square:' * filled + ':white_large_square:' * (10 - filled)
# Git agent last patrol
git_status = 'No patrol data'
if git_log and os.path.exists(git_log):
with open(git_log) as f:
lines = f.readlines()
patrols = [l.strip() for l in lines if 'PATROL:' in l]
if patrols:
last = patrols[-1]
git_status = last.split('] ', 1)[-1] if '] ' in last else last
# Alerts
alerts = []
offline = s.get('fleet_offline', [])
if offline:
alerts.append(f":rotating_light: *Nodes offline*: {', '.join(offline)}")
if s.get('failed_units', 0) > 0:
alerts.append(f":warning: *{s['failed_units']} failed systemd units*")
throttled = s.get('throttled_nodes', [])
if throttled:
alerts.append(f":fire: *Throttled*: {', '.join(throttled)}")
if s.get('avg_temp_c', 0) > 70:
alerts.append(f":thermometer: *High temp*: {s['avg_temp_c']}C avg")
if fleet_online < fleet_total:
alerts.append(f":satellite: *Fleet degraded*: {fleet_online}/{fleet_total} online")
alert_text = '\n'.join(alerts) if alerts else ':white_check_mark: All systems nominal'
# Weekly trend (last 7 days of commits)
trend_commits = []
daily_dir = os.path.dirname(daily_file)
for f in sorted(glob.glob(os.path.join(daily_dir, '*.json')))[-7:]:
try:
with open(f) as fh:
d = json.load(fh)
trend_commits.append(d.get('summary', {}).get('commits_today', 0))
except:
pass
sparkline = ''
if trend_commits:
max_c = max(trend_commits) or 1
bars = ['_', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']
sparkline = ''.join(bars[min(8, int(c / max_c * 8))] for c in trend_commits)
sparkline = f"`{sparkline}` (7d commits)"
blocks = [
{
"type": "header",
"text": {"type": "plain_text", "text": f"BlackRoad OS — Daily KPIs {data['date']}"}
},
# ── Alerts section ──
{
"type": "section",
"text": {"type": "mrkdwn", "text": alert_text}
},
{"type": "divider"},
# ── Code velocity ──
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f":rocket: *Code Velocity*\n"
f"Commits: *{s['commits_today']}*{delta('commits_today')}\n"
f"PRs merged: *{s['prs_merged_today']}*{delta('prs_merged_today')}\n"
f"PRs open: {s['prs_open']}{delta('prs_open')}\n"
f"Events: {s.get('github_events_today', 0)}{delta('github_events_today')}"},
{"type": "mrkdwn", "text": f":bar_chart: *Scale*\n"
f"LOC: *{fmt(s['total_loc'])}*{delta('total_loc')}\n"
f"Repos: *{s['repos_total']}* ({s['repos_github']} GH + {s['repos_gitea']} Gitea){delta('repos_total')}\n"
f"Languages: {s.get('github_language_count', 0)}\n"
f"{sparkline}"},
]
},
# ── Fleet + Services ──
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"{fleet_emoji} *Fleet*\n"
f"Online: *{fleet_online}/{fleet_total}*{delta('fleet_online')}\n"
f"Temp: {s.get('avg_temp_c', 0):.1f}C\n"
f"Mem: {s.get('fleet_mem_used_mb', 0)}/{s.get('fleet_mem_total_mb', 0)} MB\n"
f"Disk: {s.get('fleet_disk_used_gb', 0)}/{s.get('fleet_disk_total_gb', 0)} GB"},
{"type": "mrkdwn", "text": f":gear: *Services*\n"
f"Ollama: *{s.get('ollama_models', 0)}* models ({s.get('ollama_size_gb', 0):.1f} GB)\n"
f"Docker: {s.get('docker_containers', 0)} containers\n"
f"Systemd: {s.get('systemd_services', 0)} svc / {s.get('systemd_timers', 0)} timers\n"
f"Nginx: {s.get('nginx_sites', 0)} sites"},
]
},
# ── Autonomy + Cloud ──
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f":robot_face: *Autonomy*\n"
f"Score: *{score}/100*{delta('autonomy_score')}\n"
f"{score_bar}\n"
f"Heals: {s.get('heal_events_today', 0)} | Restarts: {s.get('service_restarts_today', 0)}\n"
f"Crons: {s.get('fleet_cron_jobs', 0)} | Uptime: {s.get('max_uptime_days', 0)}d"},
{"type": "mrkdwn", "text": f":cloud: *Cloudflare*\n"
f"Pages: {s.get('cf_pages', 0)}{delta('cf_pages')}\n"
f"D1: {s.get('cf_d1_databases', 0)} | KV: {s.get('cf_kv_namespaces', 0)}\n"
f"R2: {s.get('cf_r2_buckets', 0)}\n"
f"DBs total: {s.get('sqlite_dbs', 0)} SQLite + {s.get('postgres_dbs', 0)} PG + {s.get('cf_d1_databases', 0)} D1"},
]
},
# ── Local Mac ──
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f":computer: *Local Mac*\n"
f"CLI tools: {s.get('bin_tools', 0)} | Scripts: {s.get('home_scripts', 0)}\n"
f"Git repos: {s.get('local_git_repos', 0)}\n"
f"Disk: {s.get('mac_disk_used_gb', 0)} GB ({s.get('mac_disk_pct', 0)}%)\n"
f"Processes: {s.get('mac_processes', 0)}"},
{"type": "mrkdwn", "text": f":file_cabinet: *Data*\n"
f"SQLite DBs: {s.get('sqlite_dbs', 0)}\n"
f"Total DB rows: {fmt(s.get('total_db_rows', 0))}\n"
f"FTS5 entries: {fmt(s.get('fts5_entries', 0))}\n"
f"Packages: {s.get('brew_packages', 0)} brew / {s.get('pip_packages', 0)} pip / {s.get('npm_global_packages', 0)} npm"},
]
},
{"type": "divider"},
# ── Git agent status ──
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": f":satellite_antenna: Git Agent: _{git_status}_ | Collected {data['collected_at']}"}
]
}
]
payload = {"blocks": blocks}
print(json.dumps(payload))
PYEOF
)
if [ -z "$payload" ]; then
err "Failed to build Slack payload"
exit 1
fi
# ─── Post to Slack ───────────────────────────────────────────────────
log "Posting daily report to Slack..."
if slack_post "$payload"; then
ok "Daily report posted to Slack"
else
err "Slack post failed"
fi