211 lines
6.1 KiB
Bash
211 lines
6.1 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
# ── LOOPSTAT Panel ──────────────────────
|
|||
|
|
# Strategic view: queue, perf, triage,
|
|||
|
|
# recent cycles. 40-col × 50-row pane.
|
|||
|
|
# ────────────────────────────────────────
|
|||
|
|
|
|||
|
|
REPO="$HOME/Timmy-Time-dashboard"
|
|||
|
|
QUEUE="$REPO/.loop/queue.json"
|
|||
|
|
RETRO="$REPO/.loop/retro/cycles.jsonl"
|
|||
|
|
TRIAGE_R="$REPO/.loop/retro/triage.jsonl"
|
|||
|
|
DEEP_R="$REPO/.loop/retro/deep-triage.jsonl"
|
|||
|
|
SUMMARY="$REPO/.loop/retro/summary.json"
|
|||
|
|
QUARANTINE="$REPO/.loop/quarantine.json"
|
|||
|
|
STATE="$REPO/.loop/state.json"
|
|||
|
|
|
|||
|
|
B='\033[1m' ; D='\033[2m' ; R='\033[0m'
|
|||
|
|
G='\033[32m' ; Y='\033[33m' ; RD='\033[31m'
|
|||
|
|
C='\033[36m' ; M='\033[35m'
|
|||
|
|
|
|||
|
|
W=$(tput cols 2>/dev/null || echo 40)
|
|||
|
|
hr() { printf "${D}"; printf '─%.0s' $(seq 1 "$W"); printf "${R}\n"; }
|
|||
|
|
|
|||
|
|
while true; do
|
|||
|
|
clear
|
|||
|
|
echo -e "${B}${M} ◈ LOOPSTAT${R} ${D}$(date '+%H:%M')${R}"
|
|||
|
|
hr
|
|||
|
|
|
|||
|
|
# ── PERFORMANCE ──────────────────────
|
|||
|
|
python3 -c "
|
|||
|
|
import json, os
|
|||
|
|
f = '$SUMMARY'
|
|||
|
|
if not os.path.exists(f):
|
|||
|
|
print(' \033[2m(no perf data yet)\033[0m')
|
|||
|
|
raise SystemExit
|
|||
|
|
s = json.load(open(f))
|
|||
|
|
rate = s.get('success_rate', 0)
|
|||
|
|
avg = s.get('avg_duration_seconds', 0)
|
|||
|
|
total = s.get('total_cycles', 0)
|
|||
|
|
merged = s.get('total_prs_merged', 0)
|
|||
|
|
added = s.get('total_lines_added', 0)
|
|||
|
|
removed = s.get('total_lines_removed', 0)
|
|||
|
|
|
|||
|
|
rc = '\033[32m' if rate >= .8 else '\033[33m' if rate >= .5 else '\033[31m'
|
|||
|
|
am, asec = divmod(avg, 60)
|
|||
|
|
print(f' {rc}{rate*100:.0f}%\033[0m ok \033[1m{am:.0f}m{asec:02.0f}s\033[0m avg {total} cyc')
|
|||
|
|
print(f' \033[32m{merged}\033[0m PRs \033[32m+{added}\033[0m/\033[31m-{removed}\033[0m lines')
|
|||
|
|
|
|||
|
|
bt = s.get('by_type', {})
|
|||
|
|
parts = []
|
|||
|
|
for t in ['bug','feature','refactor']:
|
|||
|
|
i = bt.get(t, {})
|
|||
|
|
if i.get('count', 0):
|
|||
|
|
sr = i.get('success_rate', 0)
|
|||
|
|
parts.append(f'{t[:3]}:{sr*100:.0f}%')
|
|||
|
|
if parts:
|
|||
|
|
print(f' \033[2m{\" \".join(parts)}\033[0m')
|
|||
|
|
" 2>/dev/null
|
|||
|
|
|
|||
|
|
hr
|
|||
|
|
|
|||
|
|
# ── QUEUE ────────────────────────────
|
|||
|
|
echo -e "${B}${Y} QUEUE${R}"
|
|||
|
|
python3 -c "
|
|||
|
|
import json, os
|
|||
|
|
f = '$QUEUE'
|
|||
|
|
if not os.path.exists(f):
|
|||
|
|
print(' \033[2m(no queue yet)\033[0m')
|
|||
|
|
raise SystemExit
|
|||
|
|
q = json.load(open(f))
|
|||
|
|
if not q:
|
|||
|
|
print(' \033[2m(empty — needs triage)\033[0m')
|
|||
|
|
raise SystemExit
|
|||
|
|
|
|||
|
|
types = {}
|
|||
|
|
for item in q:
|
|||
|
|
t = item.get('type','?')
|
|||
|
|
types[t] = types.get(t, 0) + 1
|
|||
|
|
ts = ' '.join(f'{t[0].upper()}:{n}' for t,n in sorted(types.items()) if t != 'philosophy')
|
|||
|
|
print(f' \033[1m{len(q)}\033[0m ready \033[2m{ts}\033[0m')
|
|||
|
|
print()
|
|||
|
|
for i, item in enumerate(q[:8]):
|
|||
|
|
n = item['issue']
|
|||
|
|
s = item.get('score', 0)
|
|||
|
|
title = item.get('title', '?')
|
|||
|
|
t = item.get('type', '?')
|
|||
|
|
ic = {'bug':'\033[31m●','feature':'\033[32m◆','refactor':'\033[36m○'}.get(t, '\033[2m·')
|
|||
|
|
bar = '█' * s + '░' * (9 - s)
|
|||
|
|
ptr = '\033[1m→' if i == 0 else f'\033[2m{i+1}'
|
|||
|
|
# Truncate title to fit: 40 - 2(pad) - 2(ptr) - 2(ic) - 5(#num) - 1 = 28
|
|||
|
|
tit = title[:24]
|
|||
|
|
print(f' {ptr}\033[0m {ic}\033[0m \033[33m#{n}\033[0m {tit}')
|
|||
|
|
if len(q) > 8:
|
|||
|
|
print(f' \033[2m +{len(q)-8} more\033[0m')
|
|||
|
|
" 2>/dev/null
|
|||
|
|
|
|||
|
|
hr
|
|||
|
|
|
|||
|
|
# ── TRIAGE ───────────────────────────
|
|||
|
|
echo -e "${B}${G} TRIAGE${R}"
|
|||
|
|
python3 -c "
|
|||
|
|
import json, os
|
|||
|
|
from datetime import datetime, timezone
|
|||
|
|
|
|||
|
|
cycle = '?'
|
|||
|
|
if os.path.exists('$STATE'):
|
|||
|
|
try: cycle = json.load(open('$STATE')).get('cycle','?')
|
|||
|
|
except: pass
|
|||
|
|
|
|||
|
|
def ago(ts):
|
|||
|
|
if not ts: return 'never'
|
|||
|
|
try:
|
|||
|
|
dt = datetime.fromisoformat(ts)
|
|||
|
|
if dt.tzinfo is None:
|
|||
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|||
|
|
m = int((datetime.now(timezone.utc) - dt).total_seconds() / 60)
|
|||
|
|
if m < 60: return f'{m}m ago'
|
|||
|
|
if m < 1440: return f'{m//60}h{m%60}m ago'
|
|||
|
|
return f'{m//1440}d ago'
|
|||
|
|
except: return '?'
|
|||
|
|
|
|||
|
|
# Fast
|
|||
|
|
fast_ago = 'never'
|
|||
|
|
if os.path.exists('$TRIAGE_R'):
|
|||
|
|
lines = open('$TRIAGE_R').read().strip().splitlines()
|
|||
|
|
if lines:
|
|||
|
|
try:
|
|||
|
|
last = json.loads(lines[-1])
|
|||
|
|
fast_ago = ago(last.get('timestamp',''))
|
|||
|
|
except: pass
|
|||
|
|
|
|||
|
|
# Deep
|
|||
|
|
deep_ago = 'never'
|
|||
|
|
timmy = ''
|
|||
|
|
if os.path.exists('$DEEP_R'):
|
|||
|
|
lines = open('$DEEP_R').read().strip().splitlines()
|
|||
|
|
if lines:
|
|||
|
|
try:
|
|||
|
|
last = json.loads(lines[-1])
|
|||
|
|
deep_ago = ago(last.get('timestamp',''))
|
|||
|
|
timmy = last.get('timmy_feedback','')[:60]
|
|||
|
|
except: pass
|
|||
|
|
|
|||
|
|
# Next
|
|||
|
|
try:
|
|||
|
|
c = int(cycle)
|
|||
|
|
nf = 5 - (c % 5)
|
|||
|
|
nd = 20 - (c % 20)
|
|||
|
|
except:
|
|||
|
|
nf = nd = '?'
|
|||
|
|
|
|||
|
|
print(f' Fast {fast_ago:<12s} \033[2mnext:{nf}c\033[0m')
|
|||
|
|
print(f' Deep {deep_ago:<12s} \033[2mnext:{nd}c\033[0m')
|
|||
|
|
if timmy:
|
|||
|
|
# wrap at ~36 chars
|
|||
|
|
print(f' \033[35mTimmy:\033[0m')
|
|||
|
|
t = timmy
|
|||
|
|
while t:
|
|||
|
|
print(f' \033[2m{t[:36]}\033[0m')
|
|||
|
|
t = t[36:]
|
|||
|
|
|
|||
|
|
# Quarantine
|
|||
|
|
if os.path.exists('$QUARANTINE'):
|
|||
|
|
try:
|
|||
|
|
qd = json.load(open('$QUARANTINE'))
|
|||
|
|
if qd:
|
|||
|
|
qs = ','.join(f'#{k}' for k in list(qd.keys())[:4])
|
|||
|
|
print(f' \033[31mQuarantined:{len(qd)}\033[0m {qs}')
|
|||
|
|
except: pass
|
|||
|
|
" 2>/dev/null
|
|||
|
|
|
|||
|
|
hr
|
|||
|
|
|
|||
|
|
# ── RECENT CYCLES ────────────────────
|
|||
|
|
echo -e "${B}${D} CYCLES${R}"
|
|||
|
|
python3 -c "
|
|||
|
|
import json, os
|
|||
|
|
f = '$RETRO'
|
|||
|
|
if not os.path.exists(f):
|
|||
|
|
print(' \033[2m(none yet)\033[0m')
|
|||
|
|
raise SystemExit
|
|||
|
|
lines = open(f).read().strip().splitlines()
|
|||
|
|
recent = []
|
|||
|
|
for l in lines[-12:]:
|
|||
|
|
try: recent.append(json.loads(l))
|
|||
|
|
except: continue
|
|||
|
|
if not recent:
|
|||
|
|
print(' \033[2m(none yet)\033[0m')
|
|||
|
|
raise SystemExit
|
|||
|
|
for e in reversed(recent):
|
|||
|
|
cy = e.get('cycle','?')
|
|||
|
|
ok = e.get('success', False)
|
|||
|
|
iss = e.get('issue','')
|
|||
|
|
dur = e.get('duration', 0)
|
|||
|
|
pr = e.get('pr','')
|
|||
|
|
reason = e.get('reason','')[:18]
|
|||
|
|
|
|||
|
|
ic = '\033[32m✓\033[0m' if ok else '\033[31m✗\033[0m'
|
|||
|
|
ds = f'{dur//60}m' if dur else '-'
|
|||
|
|
ix = f'#{iss}' if iss else ' — '
|
|||
|
|
if ok:
|
|||
|
|
det = f'PR#{pr}' if pr else ''
|
|||
|
|
else:
|
|||
|
|
det = reason
|
|||
|
|
print(f' {ic} {cy:<3} {ix:<5s} {ds:>4s} \033[2m{det}\033[0m')
|
|||
|
|
" 2>/dev/null
|
|||
|
|
|
|||
|
|
hr
|
|||
|
|
echo -e "${D} ↻ 10s${R}"
|
|||
|
|
sleep 10
|
|||
|
|
done
|