AutoCrib PM Hub
Loading...
⚠ New rework detected —
Sprint Health
Loading...
Not Started
Dev In Progress
In Testing (Dev)
In Testing (QA)
Ready to Deploy
Done
Dev Only
Alert
Warning
At Risk
Team
Loading sprint data
`; const win = window.open('', '_blank'); win.document.write(html); win.document.close(); } // ── UPDATE BUTTON ───────────────────────────────────────── async function requestUpdate() { const btn=document.getElementById('update-btn'), dot=document.getElementById('cache-dot'), lbl=document.getElementById('cache-label'), note=document.getElementById('rebuild-note'); btn.classList.add('queued'); btn.textContent='↻ Updating...'; dot.className='cache-dot stale'; note.textContent=''; const startTime=Date.now(); let tickInterval; try { tickInterval=setInterval(()=>{ const elapsedSec=Math.round((Date.now()-startTime)/1000); btn.textContent=`↻ Building... (${elapsedSec}s)`; },1000); // The rebuild endpoint now runs synchronously and can take 20-90s+ // for ~30 tickets (Jira fetches + Anthropic re-summarization). const ctrl=new AbortController(); const timeout=setTimeout(()=>ctrl.abort(),5*60*1000); // 5 min hard cap const res = await fetch(`${WORKER}/cache/sprint/rebuild`, {method:'POST', signal:ctrl.signal}); clearTimeout(timeout); clearInterval(tickInterval); const data = await res.json(); if (res.status===429) { btn.classList.remove('queued'); btn.textContent='↻ Update'; note.textContent = `· Rate limited — ${data.message || 'try again later'}`; return; } if (!res.ok || data.status==='Sprint rebuild failed') { btn.classList.remove('queued'); btn.textContent='↻ Update'; note.textContent = `· Rebuild failed: ${data.error || data.message || 'unknown error'}`; return; } btn.classList.remove('queued'); btn.textContent='↻ Update'; await init(); } catch(e){ clearInterval(tickInterval); btn.classList.remove('queued'); btn.textContent='↻ Update'; note.textContent = e.name==='AbortError' ? '· Rebuild timed out after 5 minutes — it may have partially completed. Reload to check.' : '· Rebuild request failed.'; } } // ── REWORK BANNER ───────────────────────────────────────── function renderReworkBanner(tickets) { const rework=tickets.filter(t=>t.hasNewSubtasks); const banner=document.getElementById('rework-banner'); if(rework.length===0){banner.classList.remove('show');return;} document.getElementById('rework-banner-tickets').innerHTML=rework.map(t=>`${t.key} +${t.newSubtaskCount}`).join(''); banner.classList.add('show'); } function scrollToAndExpand(key) { const ticket=allTickets.find(t=>t.key===key); if(ticket&¤tTeam!=='all'&&ticket.team!==currentTeam)setTeamFilter('all'); setTimeout(()=>{const w=document.getElementById(`wrapper-${key}`);if(w){w.scrollIntoView({behavior:'smooth',block:'center'});if(!w.classList.contains('expanded'))toggleTicket(key);}},100); } // ── SPRINT PROGRESS ─────────────────────────────────────── function renderSprintProgress(startStr, endStr, tickets) { const start=new Date(startStr), end=new Date(endStr), now=new Date(); const totalBizDays=countBizDays(start,end); const effectiveNow=now>end?end:now; currentSprintDay=Math.min(countBizDays(start,effectiveNow),totalBizDays); const daysRemaining=Math.max(totalBizDays-currentSprintDay,0); const timePct=Math.round(Math.min(Math.max(now-start,0),end-start)/(end-start)*100); const closingTickets=tickets.filter(t=>!t.qaNotPlanned); const devOnlyTickets=tickets.filter(t=>t.qaNotPlanned); const closedNormal=closingTickets.filter(t=>isDone(t)).length; const closingPct=closingTickets.length>0?Math.round(closedNormal/closingTickets.length*100):0; const devOnlyComplete=devOnlyTickets.filter(t=>isDone(t)).length; const devOnlyPct=devOnlyTickets.length>0?Math.round(devOnlyComplete/devOnlyTickets.length*100):0; document.getElementById('sprint-day').textContent=`Day ${currentSprintDay} of ${totalBizDays}`; document.getElementById('time-bar').style.width=`${timePct}%`; document.getElementById('time-pct').textContent=`${timePct}%`; document.getElementById('tickets-bar').style.width=`${closingPct}%`; document.getElementById('tickets-pct').textContent=`${closingPct}%`; document.getElementById('devonly-bar').style.width=`${devOnlyPct}%`; document.getElementById('devonly-pct').textContent=`${devOnlyPct}%`; const gap=timePct-closingPct; const bar=document.getElementById('tickets-bar'),pct=document.getElementById('tickets-pct'); if(gap>25){bar.style.background='#EC2224';pct.style.color='#EC2224';} else if(gap>10){bar.style.background='#E8720C';pct.style.color='#E8720C';} else{bar.style.background='#3B6D11';pct.style.color='#3B6D11';} document.getElementById('sprint-days-label').textContent=`${daysRemaining} business day${daysRemaining!==1?'s':''} remaining · ends ${end.toLocaleDateString('en-US',{month:'short',day:'numeric'})}`; const atRisk=closingTickets.filter(t=>daysRemaining<=2&&['Backlog','Prioritized','In Progress'].includes(t.status)); if(atRisk.length>0){document.getElementById('deadline-warning').textContent=`⚠ ${atRisk.length} ticket${atRisk.length>1?'s':''} not started or in progress with ${daysRemaining} business day${daysRemaining!==1?'s':''} remaining`;document.getElementById('deadline-warning').classList.add('show');} document.getElementById('sprint-progress').style.display='block'; } function countBizDays(start,end) { let count=0; const cur=new Date(start); const target=new Date(end); cur.setHours(0,0,0,0); target.setHours(23,59,59,999); while(cur<=target){if(cur.getDay()!==0&&cur.getDay()!==6)count++;cur.setDate(cur.getDate()+1);} return count; } // ── METRICS ─────────────────────────────────────────────── function renderMetrics(tickets) { const closing = tickets.filter(t=>!t.qaNotPlanned); const devOnly = tickets.filter(t=>t.qaNotPlanned); const redResources = buildRedResources(tickets); // Committed Done — uses isDone() with multi-project logic const doneTickets = closing.filter(t=>isDone(t)); const donePts = doneTickets.reduce((s,t)=>s+(t.points||0),0); const totalPts = closing.reduce((s,t)=>s+(t.points||0),0); // Dev Only status breakdown const devOnlyDone = devOnly.filter(t=>isDone(t)); const devOnlyInProgress = devOnly.filter(t=>!isDone(t)); const devOnlyPts = devOnly.reduce((s,t)=>s+(t.points||0),0); const devOnlyDonePts = devOnlyDone.reduce((s,t)=>s+(t.points||0),0); // In Testing Dev (Dev/Testing statuses) const inTestingDev = tickets.filter(t=>getStatusGroup(t)==='testing-dev'&&!isDone(t)); const inTestingDevPts = inTestingDev.reduce((s,t)=>s+(t.points||0),0); // In Testing QA (QA/Ready for Deployment to QA) const inTestingQA = tickets.filter(t=>getStatusGroup(t)==='testing-qa'&&!isDone(t)); const inTestingQAPts = inTestingQA.reduce((s,t)=>s+(t.points||0),0); // Missing points (non-done, non-dev-only) const missingPts = tickets.filter(t=>!t.points&&!isDone(t)&&!t.qaNotPlanned).length; // Rework const rework = tickets.filter(t=>t.hasNewSubtasks).length; document.getElementById('metrics').innerHTML = `
Committed Done
${doneTickets.length} / ${closing.length}
${donePts} / ${totalPts} pts
Dev Only
${devOnlyDone.length} / ${devOnly.length}
${devOnlyDonePts} / ${devOnlyPts} pts
In Testing (Dev)
${inTestingDev.length}
${inTestingDevPts} pts
lower env testing
In Testing (QA)
${inTestingQA.length}
${inTestingQAPts} pts
higher env testing
Missing Points
${missingPts}
no story points
${rework>0?`
New Rework
${rework}
new subtasks added
`:`
Points Done
${donePts}
of ${totalPts} committed pts
`} `; } // ── FILTERS ─────────────────────────────────────────────── function updateFilterCounts(tickets) { document.getElementById('fc-all').textContent=tickets.length; document.getElementById('fc-CR').textContent=tickets.filter(t=>t.team==='Customer Response').length; document.getElementById('fc-Sync').textContent=tickets.filter(t=>t.team==='MS Sync Replacement').length; document.getElementById('fc-GCC').textContent=tickets.filter(t=>t.team==='FedRAMP').length; document.getElementById('fc-EAU').textContent=tickets.filter(t=>t.team==='EAU').length; } function setTeamFilter(team) { currentTeam=team; document.querySelectorAll('.filter-btn').forEach(b=>b.classList.toggle('active',b.dataset.team===team)); renderContent(allTickets); } // ── RENDER CONTENT ──────────────────────────────────────── function renderContent(tickets) { const filtered=currentTeam==='all'?tickets:tickets.filter(t=>t.team===currentTeam); if(filtered.length===0){document.getElementById('content').innerHTML='
No tickets match this filter
';return;} const redResources=buildRedResources(filtered); const groups={}; TEAM_ORDER.forEach(t=>{groups[t]=[];}); filtered.forEach(ticket=>{const t=ticket.team||'Unknown';if(!groups[t])groups[t]=[];groups[t].push(ticket);}); // Sort within each group: rework > red > orange > yellow > normal > done; then workflow order const alertOrder={red:0,orange:1,yellow:2,normal:3,done:4}; const workflowOrder=['In Progress','Ready for PR','In Code Review','Dev','Testing','QA','Ready for Deployment to QA','Ready for Deployment','Ready for Deployment to Live','Prioritized','Backlog','Deployed','Closed']; for(const team of TEAM_ORDER){ if(groups[team]){ groups[team].sort((a,b)=>{ if(a.hasNewSubtasks&&!b.hasNewSubtasks)return -1; if(!a.hasNewSubtasks&&b.hasNewSubtasks)return 1; const aAlert=getFinalAlert(a,redResources).level; const bAlert=getFinalAlert(b,redResources).level; const aO=alertOrder[aAlert]??3, bO=alertOrder[bAlert]??3; if(aO!==bO)return aO-bO; return workflowOrder.indexOf(a.status)-workflowOrder.indexOf(b.status); }); } } let html=''; for(const team of TEAM_ORDER){ const group=groups[team]; if(!group||group.length===0)continue; const cfg=TEAM_CONFIG[team]||TEAM_CONFIG['Unknown']; const active=group.filter(t=>!isDone(t)&&getStatusGroup(t)!=='not-started').length; const done=group.filter(t=>isDone(t)).length; const devOnly=group.filter(t=>t.qaNotPlanned).length; const rework=group.filter(t=>t.hasNewSubtasks).length; const alerts=group.filter(t=>getFinalAlert(t,redResources).level==='red').length; const warnings=group.filter(t=>getFinalAlert(t,redResources).level==='orange').length; const teamPts=group.filter(t=>!t.qaNotPlanned).reduce((s,t)=>s+(t.points||0),0); html+=`
${cfg.label}
${group.length} tickets ${teamPts>0?`${teamPts} pts`:''}
${active} active ${done} done ${devOnly>0?`${devOnly} dev only`:''} ${alerts>0?`${alerts} alert`:''} ${warnings>0?`${warnings} warning`:''} ${rework>0?`${rework} rework`:''}
${group.map(t=>renderTicketWrapper(t,redResources)).join('')}
`; } document.getElementById('content').innerHTML=html; expandedTickets.forEach(key=>{const w=document.getElementById(`wrapper-${key}`);if(w){w.classList.add('expanded');if(loadedSubtasks[key])renderSubtaskPanel(key,loadedSubtasks[key]);}}); } // ── TICKET WRAPPER ──────────────────────────────────────── function renderTicketWrapper(ticket, redResources) { const status=ticket.status||''; const sc='s-'+status.replace(/ /g,'-').replace(/[^a-zA-Z0-9-]/g,''); const spc='sp-'+status.replace(/ /g,'-').replace(/[^a-zA-Z0-9-]/g,''); const a=ticket.analysis||{}; const alert=getFinalAlert(ticket,redResources); const done=isDone(ticket); const jiraUrl=`https://autocrib.atlassian.net/browse/${ticket.key}`; const missingPoints=!ticket.points&&!done&&!ticket.qaNotPlanned; const initials=(ticket.assignee||'UN').split(' ').map(n=>n[0]).join('').substring(0,2).toUpperCase(); const avatarColor=stringToColor(ticket.assignee||''); const qaInitials=ticket.qaTester?ticket.qaTester.split(' ').map(n=>n[0]).join('').substring(0,2).toUpperCase():null; const qaColor=ticket.qaTester?stringToColor(ticket.qaTester):null; const sizeBadge=ticket.points?`${ticket.points}pts`:done?'':ticket.qaNotPlanned?'':`No pts`; const devOnlyBadge=ticket.qaNotPlanned?`Dev Only`:''; let subtaskChip=''; if(ticket.hasNewSubtasks)subtaskChip=`⚠ +${ticket.newSubtaskCount} new`; else if(ticket.subtaskCount>0)subtaskChip=`↳ ${ticket.subtaskCount} subtask${ticket.subtaskCount>1?'s':''}`; // Alert flag pills — show ALL alerts as stacked pills const allAlerts = getAllAlerts(ticket, redResources); let alertPill=''; if(allAlerts.length > 0){ alertPill = allAlerts.map(a => { if(a.level === 'qa-passed'){ return `✓ ${escHtml(a.message)}`; } return `${escHtml(a.message)}`; }).join(' '); } // Date display let datesHtml=''; if(ticket.devStartDate||ticket.devEndDate||ticket.qaStartDate||ticket.qaEndDate){ const fmt=(d)=>{if(!d)return'—';const dt=new Date(d.split('T')[0]);return dt.toLocaleDateString('en-US',{month:'short',day:'numeric'});}; const parts=[]; if(ticket.devStartDate||ticket.devEndDate)parts.push(`Dev: ${fmt(ticket.devStartDate)} → ${fmt(ticket.devEndDate)}`); if(ticket.qaStartDate||ticket.qaEndDate)parts.push(`QA: ${fmt(ticket.qaStartDate)} → ${fmt(ticket.qaEndDate)}`); if(parts.length)datesHtml=`
${parts.join('')}
`; } // Wrapper classes — use highest severity let wrapperClass=''; if(ticket.hasNewSubtasks) wrapperClass=' alert-state-rework'; else if(alert.level==='red') wrapperClass=' alert-state-red'; else if(alert.level==='orange') wrapperClass=' alert-state-orange'; else if(alert.level==='yellow') wrapperClass=' alert-state-yellow'; else if(alert.level==='qa-passed') wrapperClass=' alert-state-qa-passed'; else if(ticket.qaNotPlanned&&!done) wrapperClass=' dev-only'; // Alert dot let dotHtml=''; if(ticket.hasNewSubtasks)dotHtml=`
`; else if(alert.level==='red')dotHtml=`
`; else if(alert.level==='orange')dotHtml=`
`; else if(alert.level==='yellow')dotHtml=`
`; else if(missingPoints)dotHtml=`
`; return `
${dotHtml}
${escHtml(shortStatus(status))} ${sizeBadge}${devOnlyBadge}${subtaskChip} ${alertPill}
${escHtml(ticket.summary)}
${a.plain_english?`
${escHtml(a.plain_english)}
`:''} ${datesHtml}
${initials}
${escHtml(ticket.assignee||'Unassigned')}
${qaInitials?`
${qaInitials}
QA: ${escHtml(ticket.qaTester)}
`:''}
Loading subtasks...
`; } // ── TOGGLE / SUBTASKS ───────────────────────────────────── async function toggleTicket(key) { const wrapper=document.getElementById(`wrapper-${key}`);if(!wrapper)return; if(wrapper.classList.contains('expanded')){wrapper.classList.remove('expanded');expandedTickets.delete(key);return;} wrapper.classList.add('expanded');expandedTickets.add(key); if(loadedSubtasks[key]){renderSubtaskPanel(key,loadedSubtasks[key]);return;} try{const res=await fetch(`${WORKER}/jira/subtasks/${key}`);const data=await res.json();loadedSubtasks[key]=data;renderSubtaskPanel(key,data);} catch(err){const p=document.getElementById(`subtasks-${key}`);if(p)p.innerHTML=`
Failed to load subtasks
`;} } function renderSubtaskPanel(key, data) { const panel=document.getElementById(`subtasks-${key}`);if(!panel)return; if(!data.subtasks||data.subtasks.length===0){panel.innerHTML=`
No subtasks on this ticket
`;return;} const source=data.source==='cache'?`cached ${Math.round(data.age_seconds/60)}m ago`:'live'; const newKeys=new Set(); if(sprintStartDate)data.subtasks.forEach(s=>{if(s.created&&new Date(s.created)>sprintStartDate)newKeys.add(s.key);}); const rows=data.subtasks.map(s=>{ const riskClass=`risk-${(s.analysis?.risk||'unknown').toLowerCase()}`; const sp=`sp-${(s.status||'').replace(/ /g,'-').replace(/[^a-zA-Z0-9-]/g,'')}`; const isNew=newKeys.has(s.key); return `
${s.key}${isNew?`NEW`:''}${escHtml(shortStatus(s.status||''))}
${escHtml(s.summary)}
${s.analysis?.plain_english?`
${escHtml(s.analysis.plain_english)}
`:''}
${s.analysis?.risk||'?'}${escHtml(s.assignee||'Unassigned')}
`; }).join(''); panel.innerHTML=`
Subtasks ${data.total}${newKeys.size>0?`+${newKeys.size} new this sprint`:''}${source}
${rows}
`; } // ── HELPERS ─────────────────────────────────────────────── function showError(t,d){document.getElementById('content').innerHTML=`
${escHtml(t)}${d?`
${escHtml(d)}`:''}
`;} function escHtml(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} function stringToColor(str){const p=['#EC2224','#185FA5','#3B6D11','#854F0B','#5B21B6','#065F46','#92400E','#1D4ED8'];let h=0;for(let i=0;i { const cell = e.target.closest('.cap-cell'); if (cell && cell.dataset.tooltip) { capTooltip.textContent = cell.dataset.tooltip; capTooltip.style.display = 'block'; } }); document.addEventListener('mouseout', e => { if (e.target.closest('.cap-cell')) { capTooltip.style.display = 'none'; } }); document.addEventListener('mousemove', e => { if (capTooltip.style.display === 'block') { const x = e.clientX, y = e.clientY; const tw = capTooltip.offsetWidth, th = capTooltip.offsetHeight; const vw = window.innerWidth, vh = window.innerHeight; // Position below cursor, flip up if too close to bottom const top = y + 16 + th > vh ? y - th - 8 : y + 16; // Position right of cursor, flip left if too close to right edge const left = x + tw + 16 > vw ? x - tw - 8 : x + 12; capTooltip.style.top = top + 'px'; capTooltip.style.left = left + 'px'; } }); } init();