/ scripts / update-fleet-stats.sh
update-fleet-stats.sh
  1  #!/usr/bin/env bash
  2  # update-fleet-stats.sh — Update fleet dashboard from beads-hub data
  3  # Idempotent, safe to run every 5 minutes via cron.
  4  # Requires: bd CLI at ~/.local/bin/bd, python3, git
  5  set -euo pipefail
  6  
  7  BD="${HOME}/.local/bin/bd"
  8  BEADS_HUB="${HOME}/.openclaw/workspaces/beads-hub"
  9  DOCS_DIR="$(cd "$(dirname "$0")/.." && pwd)"
 10  FLEET_HTML="${DOCS_DIR}/fleet/index.html"
 11  
 12  if [ ! -f "$BD" ]; then echo "ERROR: bd CLI not found at $BD" >&2; exit 1; fi
 13  if [ ! -f "$FLEET_HTML" ]; then echo "ERROR: fleet/index.html not found" >&2; exit 1; fi
 14  
 15  # Pull latest data
 16  cd "$BEADS_HUB" && git pull -q 2>/dev/null || true
 17  cd "$DOCS_DIR" && git pull -q 2>/dev/null || true
 18  
 19  # Generate updated HTML via Python (reads bd JSON, patches index.html)
 20  python3 << 'PYEOF'
 21  import json, subprocess, sys, re
 22  from datetime import datetime, timezone, timedelta
 23  
 24  BD = sys.path  # unused, just use the env
 25  bd = "${HOME}/.local/bin/bd".replace("${HOME}", __import__("os").environ["HOME"])
 26  beads_hub = "${HOME}/.openclaw/workspaces/beads-hub".replace("${HOME}", __import__("os").environ["HOME"])
 27  fleet_html = "${DOCS_DIR}/fleet/index.html".replace("${DOCS_DIR}", __import__("os").environ.get("DOCS_DIR", ""))
 28  
 29  import os
 30  bd = os.path.expanduser("~/.local/bin/bd")
 31  beads_hub = os.path.expanduser("~/.openclaw/workspaces/beads-hub")
 32  docs_dir = os.path.dirname(os.path.dirname(os.path.abspath("${0}")))
 33  # Re-derive docs_dir properly
 34  script_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in dir() else None
 35  # Just use env
 36  fleet_html = os.environ.get("FLEET_HTML", "")
 37  if not fleet_html:
 38      # Fallback
 39      fleet_html = os.path.expanduser("~/.openclaw/workspaces/docs/fleet/index.html")
 40  
 41  def bd_list(status=None):
 42      cmd = [bd, "list", "--json"]
 43      if status:
 44          cmd += ["--status", status]
 45      r = subprocess.run(cmd, capture_output=True, text=True, cwd=beads_hub)
 46      if r.returncode != 0:
 47          print(f"bd error: {r.stderr}", file=sys.stderr)
 48          return []
 49      return json.loads(r.stdout)
 50  
 51  now = datetime.now(timezone.utc)
 52  cutoff = now - timedelta(hours=24)
 53  
 54  all_beads = bd_list()
 55  closed_beads = bd_list("closed")
 56  open_beads = [b for b in all_beads if b["status"] == "open"]
 57  in_progress = [b for b in all_beads if b["status"] == "in_progress"]
 58  
 59  # Recent closed (last 24h)
 60  recent_closed = [b for b in closed_beads if b.get("closed_at") and b["closed_at"] > cutoff.isoformat()]
 61  recent_closed.sort(key=lambda b: b.get("closed_at", ""), reverse=True)
 62  
 63  # All-time counts
 64  total_closed = len(closed_beads)
 65  total_open = len(open_beads)
 66  total_in_progress = len(in_progress)
 67  total_all = total_closed + total_open + total_in_progress
 68  
 69  # Stats mapping (aviation metaphors)
 70  distance_nm = total_closed  # each closed bead = 1 nautical mile
 71  sorties = total_all  # total beads = total sorties
 72  fleet_ready = f"{min(5, 5)}/6"  # could be dynamic later
 73  
 74  # Agent mapping for flight log
 75  def guess_agent(title):
 76      t = title.lower()
 77      if "research:" in t: return "ROMANOV"
 78      if "container" in t or "ci/cd" in t or "deploy" in t or "fix" in t or "github" in t or "pipeline" in t: return "PLTOPS"
 79      if "code" in t or "script" in t or "build" in t or "dao" in t or "smart contract" in t: return "CODEMONKEY"
 80      if "url" in t or "brew" in t or "summar" in t: return "BREW"
 81      if "linkedin" in t: return "LINKEDIN BRIEF"
 82      return "TOWER"
 83  
 84  # Build flight log HTML (last 10 recent closed)
 85  log_entries = []
 86  for b in recent_closed[:10]:
 87      closed_at = b.get("closed_at", "")
 88      try:
 89          t = datetime.fromisoformat(closed_at.replace("Z", "+00:00"))
 90          time_str = t.strftime("%H:%MZ")
 91      except:
 92          time_str = "??:??Z"
 93      agent = guess_agent(b["title"])
 94      bead_id = b["id"]
 95      # Truncate title
 96      title = b["title"][:60]
 97      log_entries.append(
 98          f'  <div class="log-entry">\n'
 99          f'    <span class="log-time">{time_str}</span>\n'
100          f'    <span class="log-callsign">{agent}</span>\n'
101          f'    <span class="log-mission">{title} — {bead_id}</span>\n'
102          f'    <span class="log-status"><span class="complete">✓ RTB</span></span>\n'
103          f'  </div>'
104      )
105  
106  # If no recent closed, also show in_progress as ACTIVE
107  for b in in_progress[:max(0, 10 - len(log_entries))]:
108      agent = guess_agent(b["title"])
109      title = b["title"][:60]
110      updated = b.get("updated_at", "")
111      try:
112          t = datetime.fromisoformat(updated.replace("Z", "+00:00"))
113          time_str = t.strftime("%H:%MZ")
114      except:
115          time_str = "??:??Z"
116      log_entries.append(
117          f'  <div class="log-entry">\n'
118          f'    <span class="log-time">{time_str}</span>\n'
119          f'    <span class="log-callsign">{agent}</span>\n'
120          f'    <span class="log-mission">{title} — {b["id"]}</span>\n'
121          f'    <span class="log-status"><span class="active">● ACTIVE</span></span>\n'
122          f'  </div>'
123      )
124  
125  flight_log_html = "\n".join(log_entries)
126  
127  # Read HTML
128  with open(fleet_html, "r") as f:
129      html = f.read()
130  
131  # Update timestamp in status line
132  now_str = now.strftime("%d %b %Y").upper()
133  html = re.sub(
134      r'ALL SYSTEMS NOMINAL — LAST 24H OPS SUMMARY — [^<]+',
135      f'ALL SYSTEMS NOMINAL — LAST 24H OPS SUMMARY — {now_str}',
136      html
137  )
138  
139  # Update footer timestamp
140  now_full = now.strftime("%d %b %Y %H:%MZ").upper()
141  html = re.sub(
142      r'UPDATED: [^·]+·',
143      f'UPDATED: {now_full} · ',
144      html
145  )
146  
147  # Update stats bar values (order: distance, fuel, passengers, flight hrs, sorties, fleet ready)
148  stat_values = [
149      str(distance_nm),           # Distance Flown (nm)
150      f"{total_all * 35:.1f}K",   # Fuel Burned (rough token estimate)
151      str(total_open + total_in_progress),  # Passengers (active beads)
152      f"{total_all * 0.6:.1f}",   # Flight Hours
153      str(sorties),               # Sorties Flown
154      fleet_ready,                # Fleet Ready
155  ]
156  
157  # Replace stat values by finding stat-value divs followed by stat-unit/stat-label
158  # We'll do targeted replacements based on stat-label text
159  label_value_map = {
160      "Distance Flown": str(distance_nm),
161      "Fuel Burned": f"{total_all * 35:.0f}K",
162      "Passengers": str(total_open + total_in_progress),
163      "Flight Hours": f"{total_all * 0.6:.1f}",
164      "Sorties Flown": str(sorties),
165      "Fleet Ready": fleet_ready,
166  }
167  
168  for label, value in label_value_map.items():
169      # Match the stat-item block containing this label and update its value
170      pattern = re.compile(
171          r'(<div class="stat-value">)[^<]*(</div>\s*<div class="stat-unit">[^<]*</div>\s*<div class="stat-label">' + re.escape(label) + r'</div>)',
172          re.DOTALL
173      )
174      html = pattern.sub(rf'\g<1>{value}\2', html)
175  
176  # Replace flight log section
177  flight_log_pattern = re.compile(
178      r'(<div class="flight-log">\n)(.*?)(</div>\s*\n\s*<div style="text-align: center)',
179      re.DOTALL
180  )
181  html = flight_log_pattern.sub(
182      rf'\g<1>{flight_log_html}\n\3',
183      html
184  )
185  
186  with open(fleet_html, "w") as f:
187      f.write(html)
188  
189  print(f"Updated fleet dashboard: {total_closed} closed, {total_open} open, {total_in_progress} in_progress, {len(recent_closed)} recent closed (24h)")
190  PYEOF
191  
192  # Commit and push if changed
193  cd "$DOCS_DIR"
194  if git diff --quiet fleet/index.html 2>/dev/null; then
195    echo "No changes to fleet dashboard."
196  else
197    git add fleet/index.html
198    git commit -m "fleet: auto-update dashboard stats $(date -u '+%Y-%m-%dT%H:%MZ')"
199    git push
200    echo "Fleet dashboard updated and pushed."
201  fi