diff --git a/src/dashboard-js-part1.ts b/src/dashboard-js-core.ts
similarity index 74%
rename from src/dashboard-js-part1.ts
rename to src/dashboard-js-core.ts
index de5ec2e..4cfa353 100644
--- a/src/dashboard-js-part1.ts
+++ b/src/dashboard-js-core.ts
@@ -1,6 +1,6 @@
/** Dashboard JS — utilities, data helpers, and state management. */
-export const DASHBOARD_JS_PART1 = `
-let S=null,selId=null,fails=0,pollT=Date.now(),prevMC=0,selCard=-1;
+export const DASHBOARD_JS_CORE = `
+let S=null,selId=null,selProjectId=null,fails=0,pollT=Date.now(),prevMC=0,selCard=-1,navCollapsed=false;
const expCards=new Set(),expMsgs=new Set();
const E=s=>s?String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'):'';
const D=ms=>{const s=Math.floor(Math.abs(ms)/1000);return s<60?s+'s':s<3600?Math.floor(s/60)+'m':Math.floor(s/3600)+'h'};
@@ -63,13 +63,21 @@ function allTeams(){
const archived=[...S.teams.filter(t=>t.status!=='active')].sort((a,b)=>b.timeUpdated-a.timeUpdated);
return{active,archived};
}
-function cur(){const{active,archived}=allTeams(),all=[...active,...archived];if(!all.length)return null;if(selId){const t=all.find(t=>t.id===selId);if(t)return t}return active[0]||all[0]}
+function allProjects(){return S?.projects?[...S.projects].sort((a,b)=>b.timeUpdated-a.timeUpdated):[]}
+function projectLabel(p){return p?(p.name||p.path||p.id):''}
+function curProject(){const ps=allProjects();if(!ps.length)return null;if(selProjectId){const p=ps.find(p=>p.id===selProjectId);if(p)return p}var t=cur();return t?ps.find(p=>p.id===t.projectId)||ps[0]:ps[0]}
+function cur(){const{active,archived}=allTeams(),all=[...active,...archived];if(!all.length)return null;if(selId){const t=all.find(t=>t.id===selId);if(t){selProjectId=t.projectId;return t}}const p=selProjectId&&S?.projects?.find(p=>p.id===selProjectId);const pt=p?[...(p.teams||[])].filter(t=>t.status==='active'):[ ];const t=pt.sort((a,b)=>b.timeUpdated-a.timeUpdated)[0]||active[0]||all[0];if(t){selId=t.id;selProjectId=t.projectId}return t}
function deriveHealth(t){
const mm=t.members||[];if(!mm.length)return{w:0,i:0,e:0,d:0,total:0};
return{w:mm.filter(m=>m.status==='busy').length,i:mm.filter(m=>m.status==='ready').length,e:mm.filter(m=>m.status==='error').length,d:mm.filter(m=>m.status==='shutdown'||m.status==='shutdown_requested').length,total:mm.length};
}
+function coarseTeamStatus(t){const h=deriveHealth(t),blocked=(t.tasks||[]).filter(x=>x.status==='blocked').length;if(h.e)return{label:'error',color:'red',dot:'bg-red-500'};if(blocked)return{label:'blocked',color:'amber',dot:'bg-amber-500'};if(h.w)return{label:'working',color:'blue',dot:'bg-blue-500'};if(h.i)return{label:'idle',color:'muted',dot:'bg-txt-500'};return{label:t.status==='active'?'empty':t.status,color:'muted',dot:'bg-base-600'}}
+function projectStatus(p){const teams=p.teams||[],counts={working:0,blocked:0,error:0,idle:0,done:0};teams.forEach(t=>{const s=coarseTeamStatus(t).label;if(s==='working')counts.working++;else if(s==='blocked')counts.blocked++;else if(s==='error')counts.error++;else if(s==='idle'||s==='empty')counts.idle++;else counts.done++});if(counts.error)return{label:'error',color:'red',dot:'bg-red-500',counts};if(counts.blocked)return{label:'blocked',color:'amber',dot:'bg-amber-500',counts};if(counts.working)return{label:'working',color:'blue',dot:'bg-blue-500',counts};return{label:'idle',color:'muted',dot:'bg-txt-500',counts}}
+function statusTitleProject(p){const s=projectStatus(p),teams=p.teams||[];return projectLabel(p)+'\\nStatus: '+s.label+'\\nTeams: '+teams.length+'\\nWorking: '+s.counts.working+' · Blocked: '+s.counts.blocked+' · Error: '+s.counts.error+' · Idle: '+s.counts.idle}
+function statusTitleTeam(t){const h=deriveHealth(t),tasks=t.tasks||[],blocked=tasks.filter(x=>x.status==='blocked').length,active=tasks.filter(x=>x.status==='in_progress').length,pending=tasks.filter(x=>x.status==='pending').length,done=tasks.filter(x=>x.status==='completed').length;return t.name+'\\nStatus: '+coarseTeamStatus(t).label+'\\nAgents: '+h.total+' total, '+h.w+' working, '+h.i+' idle, '+h.e+' error\\nTasks: '+active+' active, '+blocked+' blocked, '+pending+' pending, '+done+' done'}
+
function lastMessageFor(name,msgs){return msgs.filter(m=>m.fromName===name||m.toName===name).sort((a,b)=>b.timeCreated-a.timeCreated)[0]||null}
function activeTaskFor(name,tasks){return tasks.find(x=>x.assignee===name&&x.status==='in_progress')||tasks.find(x=>x.assignee===name&&x.status==='blocked')||null}
function blockedTaskFor(name,tasks){return tasks.find(x=>x.assignee===name&&x.status==='blocked')||null}
diff --git a/src/dashboard-js-part3.ts b/src/dashboard-js-events.ts
similarity index 74%
rename from src/dashboard-js-part3.ts
rename to src/dashboard-js-events.ts
index c1bbb1c..20443f2 100644
--- a/src/dashboard-js-part3.ts
+++ b/src/dashboard-js-events.ts
@@ -1,15 +1,32 @@
/** Dashboard JS — interaction handlers, keyboard, polling. */
-export const DASHBOARD_JS_PART3 = `
+export const DASHBOARD_JS_EVENTS = `
function toggleMsg(id){if(expMsgs.has(id))expMsgs.delete(id);else expMsgs.add(id);render()}
+function applyNavCollapse(){
+ const content=document.getElementById('content'),projects=document.getElementById('projects'),rail=document.getElementById('project-rail'),toggle=document.getElementById('nav-toggle'),expand=document.getElementById('nav-expand');
+ content.classList.toggle('nav-collapsed',navCollapsed);
+ projects.hidden=navCollapsed;
+ projects.setAttribute('aria-hidden',String(navCollapsed));
+ rail.hidden=!navCollapsed;
+ if(toggle)toggle.setAttribute('aria-expanded',String(!navCollapsed));
+ expand.setAttribute('aria-expanded',String(!navCollapsed));
+ if(document.activeElement===toggle&&navCollapsed)expand.focus();
+ if(document.activeElement===expand&&!navCollapsed&&toggle)toggle.focus();
+}
+
function render(){
rSel();const t=cur();
const empty=document.getElementById('empty'),content=document.getElementById('content');
if(!t){empty.classList.remove('hidden');empty.classList.add('flex');content.classList.add('hidden');document.getElementById('tl').classList.add('hidden');return}
empty.classList.add('hidden');empty.classList.remove('flex');content.classList.remove('hidden');
+ applyNavCollapse();
+ const p=curProject();document.getElementById('crumb').textContent=p?' / '+projectLabel(p)+' / '+t.name:'';
rHealth(t);rSum(t);rAttention(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
}
+function selectProject(id){selProjectId=id;const p=S?.projects?.find(p=>p.id===id);const t=(p?.teams||[]).filter(t=>t.status==='active').sort((a,b)=>b.timeUpdated-a.timeUpdated)[0]||(p?.teams||[])[0];if(t)selId=t.id;selCard=-1;render()}
+function selectTeam(id){selId=id;selCard=-1;render()}
+
function conn(ok){
document.getElementById('cd').className='w-[7px] h-[7px] rounded-full '+(ok?'bg-emerald-500 pulse':'bg-red-500');
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'reconnecting to dashboard state';
@@ -56,6 +73,13 @@ setInterval(function(){var t=cur();if(t)rClock(t);if(fails<3)conn(true)},1000);
// Poll every 2.5s
setInterval(poll,2500);
+document.addEventListener('click',function(e){
+ const id=e.target&&e.target.id;
+ if(id!=='nav-toggle'&&id!=='nav-expand')return;
+ navCollapsed=id==='nav-toggle';
+ applyNavCollapse();
+});
+
// Keyboard shortcuts
document.addEventListener('keydown',function(e){
const shortcutsOpen=document.getElementById('sco').classList.contains('show');
@@ -80,9 +104,6 @@ document.addEventListener('keydown',function(e){
}
});
-// Select handler
-document.getElementById('sel').addEventListener('change',function(){selId=this.value;render()});
-
// Initial poll
poll();
diff --git a/src/dashboard-js-part2.ts b/src/dashboard-js-render.ts
similarity index 86%
rename from src/dashboard-js-part2.ts
rename to src/dashboard-js-render.ts
index e777e8e..246cc37 100644
--- a/src/dashboard-js-part2.ts
+++ b/src/dashboard-js-render.ts
@@ -1,15 +1,20 @@
/** Dashboard JS — render functions. */
-export const DASHBOARD_JS_PART2 = `
+export const DASHBOARD_JS_RENDER = `
function rSel(){
- const el=document.getElementById('sel'),{active,archived}=allTeams(),c=cur();
- if(!active.length&&!archived.length){el.innerHTML='
';return}
- let h='';
- if(active.length){h+='
'}
- if(archived.length){h+='
'}
- if(el._lh!==h){el.innerHTML=h;el._lh=h}
- if(c)el.value=c.id;
+ const el=document.getElementById('projects'),ps=allProjects(),c=cur(),cp=curProject();
+ const head=renderProjectNavHeader();
+ if(!ps.length){patch(el,head+'
No projects yet
');return}
+ let h='
';
+ patch(el,h);
}
+function renderProjectNavHeader(){return '
'}
+function renderProjectSection(p,cp,c){const teams=p.teams||[];return '
'+renderProjectButton(p,cp)+''+[...teams].sort((a,b)=>b.timeUpdated-a.timeUpdated).map(t=>renderTeamLink(t,c)).join('')+'
'}
+function renderProjectButton(p,cp){const teams=p.teams||[],active=teams.filter(t=>t.status==='active').length,sel=cp&&cp.id===p.id,pss=projectStatus(p);return '
'}
+function renderTeamLink(t,c){const ss=coarseTeamStatus(t),tsel=c&&c.id===t.id;return '
'}
+
function rHealth(t){
const el=document.getElementById('hring'),h=deriveHealth(t);
if(!h.total){el.style.background='conic-gradient(#2a3144 0deg,#2a3144 360deg)';return}
diff --git a/src/dashboard.ts b/src/dashboard.ts
index 7deeb23..ae19548 100644
--- a/src/dashboard.ts
+++ b/src/dashboard.ts
@@ -1,22 +1,32 @@
import type { Database } from "bun:sqlite"
import { DASHBOARD_HEAD } from "./dashboard-html"
-import { DASHBOARD_JS_PART1 } from "./dashboard-js-part1"
-import { DASHBOARD_JS_PART2 } from "./dashboard-js-part2"
-import { DASHBOARD_JS_PART3 } from "./dashboard-js-part3"
+import { DASHBOARD_JS_CORE } from "./dashboard-js-core"
+import { DASHBOARD_JS_EVENTS } from "./dashboard-js-events"
+import { DASHBOARD_JS_RENDER } from "./dashboard-js-render"
import { log } from "./log"
/** Assemble the full dashboard HTML from parts. */
-const DASHBOARD_HTML = DASHBOARD_HEAD + "\n