#!/usr/bin/env python3 import json, re, html, textwrap from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT=Path(__file__).resolve().parents[1] TRIAGE=ROOT/'triage' OUT=ROOT/'visual_summary' OUT.mkdir(parents=True,exist_ok=True) def load(p): return json.loads(Path(p).read_text(encoding='utf-8')) def short(path): s=path.replace('/home/smlflg/Projekte/','').replace('/home/smlflg/','~/' ) return s if len(s)<=48 else '…'+s[-47:] def wrap(draw, text, font, width): words=str(text).split(); lines=[]; cur='' for w in words: test=(cur+' '+w).strip() if draw.textbbox((0,0),test,font=font)[2] <= width: cur=test else: if cur: lines.append(cur) cur=w if cur: lines.append(cur) return lines def top_names(items,n=8): return [short(x['path']) for x in items[:n]] def main(): pri=load(TRIAGE/'PROJECT_PRIORITIZATION.json') summary=load(TRIAGE/'PROJECT_PRIORITIZATION_SUMMARY.json') keep=pri['categories']['KEEP']; urgent=pri['categories']['URGENT']; merge=pri['categories']['MERGE']; pause=pri['categories']['PAUSE']; archive=pri['categories']['ARCHIVE']; token=pri['categories']['TOKEN_BURN_CANDIDATE']; human=pri['categories']['HUMAN_DECISION_REQUIRED'] bypath={r['path']:r for r in pri['projects']} def get(path): return bypath.get(path, {'path':path,'score':0,'dirty_count':0,'categories':[]}) top10=[ get('/home/smlflg/Projekte/Sidecar-Workspace/Sidecar-V7'), get('/home/smlflg/Projekte/Zentrale'), get('/home/smlflg/Projekte/ACT-agentcommandotower'), get('/home/smlflg/Projekte/AIOS-Cockpit'), get('/home/smlflg/Projekte/AgentArena'), get('/home/smlflg/Projekte/Jobs_Selbständigkeit_Geldverdienen/AgentTestUniSupport'), get('/home/smlflg/Projekte/Sidecar-Workspace/Sidecar-V8'), get('/home/smlflg/.hermes'), get('/home/smlflg/Projekte/UEBER_AGENT'), get('/home/smlflg/Projekte/PromptGarage'), ] gold=[get(p) for p in ['/home/smlflg/Projekte/Zentrale','/home/smlflg/Projekte/ACT-agentcommandotower','/home/smlflg/Projekte/AIOS-Cockpit','/home/smlflg/Projekte/Sidecar-Workspace/Sidecar-V8','/home/smlflg/.hermes']] almost=[get(p) for p in ['/home/smlflg/Projekte/Jobs_Selbständigkeit_Geldverdienen/AgentTestUniSupport','/home/smlflg/Projekte/Sidecar-Workspace/Sidecar-V7','/home/smlflg/Projekte/ClaudeCodePanel','/home/smlflg/Projekte/PromptGarage']] needs_tests=[r for r in keep if 'python' in (r.get('tech') or '').lower()][:7] tomorrow=[get('/home/smlflg/Projekte/Zentrale'), get('/home/smlflg/Projekte/ACT-agentcommandotower'), get('/home/smlflg/.hermes')] garbage=archive[:8] data={ 'title':'Samuel Project Map — Gold, Müll, Morgen', 'counts':summary, 'top10':top10,'gold':gold,'garbage':garbage,'almost_done':almost,'needs_tests':needs_tests, 'scope_creep':token[:8],'archive':archive[:8],'tomorrow':tomorrow,'human':human[:8], 'rules':[ 'KEEP = weiterführen, aber mit Verifier', 'MERGE = wertvoll, aber nicht separat weiterbauen', 'PAUSE = nicht löschen, aber keine Tokens mehr verbrennen', 'ARCHIVE = nur nach Human Gate; zuerst Manifest sichern', 'URGENT = dirty/risikoreich: morgen zuerst stabilisieren' ] } (OUT/'learning_graphic_data.json').write_text(json.dumps(data,indent=2,ensure_ascii=False),encoding='utf-8') W,H=1800,2400 bg='#08111f'; card='#101b2d'; stroke='#26364f'; text='#e5eefc'; muted='#94a3b8' img=Image.new('RGB',(W,H),bg); d=ImageDraw.Draw(img) def font(size,bold=False): p='/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf' if bold else '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf' return ImageFont.truetype(p,size) f_title=font(60,True); f_h=font(36,True); f=font(25); f_s=font(20); f_tiny=font(17) d.text((70,55),'🧭 Samuel Project Map',fill='#67e8f9',font=f_title) d.text((72,130),'Was ist Gold, was ist Müll, was braucht Tests, was morgen zuerst?',fill=muted,font=f) # metric pills x=70; y=190 pills=[('KEEP',summary['KEEP'],'#22c55e'),('MERGE',summary['MERGE'],'#a78bfa'),('PAUSE',summary['PAUSE'],'#94a3b8'),('ARCHIVE',summary['ARCHIVE'],'#f97316'),('URGENT',summary['URGENT'],'#ef4444'),('TOKEN BURN',summary['TOKEN_BURN_CANDIDATE'],'#facc15')] for label,val,col in pills: d.rounded_rectangle((x,y,x+250,y+64),radius=18,fill='#0f172a',outline=col,width=3) d.text((x+18,y+14),f'{label}: {val}',fill=col,font=f_s) x+=270 def card_box(x,y,w,h,title,color,items,subtitle=None): d.rounded_rectangle((x,y,x+w,y+h),radius=24,fill=card,outline=color,width=4) d.text((x+28,y+22),title,fill=color,font=f_h) cy=y+78 if subtitle: for ln in wrap(d,subtitle,f_s,w-56)[:2]: d.text((x+28,cy),ln,fill=muted,font=f_s); cy+=27 cy+=8 for i,it in enumerate(items): name=short(it['path']) if isinstance(it,dict) else str(it) score=it.get('score') if isinstance(it,dict) else None dirty=it.get('dirty_count') if isinstance(it,dict) else None line=f'{i+1}. {name}' if score is not None: line+=f' s={score}' if dirty: line+=f' dirty={dirty}' for j,ln in enumerate(wrap(d,line,f_s,w-70)[:2]): d.text((x+30,cy),ln,fill=text if i<5 else muted,font=f_s); cy+=25 cy+=6 if cy>y+h-30: break card_box(70,290,800,535,'🏆 Deine 10 wichtigsten Projekte','#22d3ee',top10,'Das ist dein echtes Agentic-OS: Control Tower + Sidecar + Hermes + Eval + Uni.') card_box(930,290,800,535,'💎 Gold','#22c55e',gold,'Nicht neu anfangen. Diese Kerne stabilisieren und verbinden.') card_box(70,870,530,420,'🗑️ Müll / Cache','#fb923c',garbage,'Vor allem repo_cache/fixtures. Erst Manifest, dann archivieren.') card_box(635,870,530,420,'✅ Fast fertig','#84cc16',almost,'Hat klare Idee/README oder MVP-Signal. Braucht Closure statt Neubau.') card_box(1200,870,530,420,'🧪 Braucht nur Tests','#38bdf8',needs_tests,'Nicht weiter bauen, bevor Smoke-Test/pytest grün ist.') card_box(70,1335,530,455,'🌀 Scope-Creep','#facc15',token[:8],'Viele Pläne/TODOs/Agent-Spuren. Token-Burn-Gefahr.') card_box(635,1335,530,455,'📦 Archivieren','#fb7185',archive[:8],'Nur Human-Gated. Nichts löschen; erst Patch/Manifest sichern.') card_box(1200,1335,530,455,'🌅 Morgen als erstes','#c084fc',tomorrow,'1) Zentrale stabilisieren 2) ACT als Closure-Prozess 3) Hermes dirty state sichern') # rules bottom d.rounded_rectangle((70,1840,1660,455+1840),radius=24,fill='#0f172a',outline='#64748b',width=3) d.text((100,1870),'🧠 Lernregel',fill='#e879f9',font=f_h) yy=1930 for rule in data['rules']: for ln in wrap(d,'• '+rule,f,1480): d.text((110,yy),ln,fill=text,font=f); yy+=34 d.text((100,2220),'Morgen-Default: Keine neuen Projekte. Erst Zentrale + ACT + Hermes in einen verifizierbaren Loop bringen.',fill='#67e8f9',font=f) d.text((100,2280),'Human Gate bleibt hart: Archive/Merge/Commit/Push nur nach expliziter Freigabe.',fill='#fca5a5',font=f_s) img.save(OUT/'project_learning_graphic.png') # HTML companion def ul(items): return '
{html.escape(short(x["path"]))} s={x.get("score")}