All Alerts
[[ violations.length ]]
Critical
[[ violations.filter(v => v.severity === 'critical').length ]]
Warnings
[[ violations.filter(v => v.severity === 'warning').length ]]
Last Check
[[ formatTimeAgo(latestCheck?.started_at) ]]
[[ latestCheck?.status || 'N/A' ]]
Active Alerts
Loading...

No alerts match the current filter.

[[ violation.severity.toUpperCase() ]] [[ violation.item_id ]] [[ formatViolationType(violation.violation_type) ]] [[ violation.metadata.category.replace(/_/g,' ') ]]
[[ violation.title || 'No title' ]]
[[ violation.owner || 'Unassigned' ]] | [[ formatHours(violation.hours_overdue) ]] overdue
[[ violation.metadata.action ]]
Activity Log
Agent interactions, PR reviews, check runs
Loading...

No activity yet

Time Type Source Item / Summary
[[ formatTimeAgo(item._time) ]] Interaction PR Review Check Run [[ (item.platform || '').toUpperCase() ]] [[ item.repo ]] [[ formatCheckType(item.check_type) ]]
[[ formatCheckType(schedule.check_type) ]]

[[ schedule.description ]]

[[ schedule.schedule_expression ]]

Last run: [[ formatTimeAgo(schedule.last_run) ]] Never run

Weekly Timesheets
Time logged in Jira per developer, grouped by week

Loading timesheet data...

No timesheet data yet. Click "Pull Current Week" to fetch from Jira.

No results for the selected filters.
[[ formatWeekLabel(week.week_start, week.week_end) ]] Showing: [[ fmtSeconds(week.developers.reduce((s,d)=>s+d.total_seconds,0)) ]] of [[ fmtSeconds(week.team_total_seconds) ]] team total / [[ fmtSeconds(week.developers.reduce((s,d)=>s+d.total_estimate_seconds,0)) ]] est
Developer / Ticket Logged Estimated Due Date Tickets
Team Members
Map Slack user IDs ↔ Jira account IDs for automated notifications
Loading team...

No team members configured yet.

Click Auto-Reconcile to fetch users from Jira & Slack and let Claude suggest matches,
or Add Member to enter IDs manually.

Name Role Jira Account ID Slack User ID Email Status Actions
[[ member.display_name ]] [[ member.role.replace('_', ' ') ]] [[ member.jira_account_id ]] [[ member.jira_display_name ]] Not set [[ member.slack_user_id ]] [[ member.slack_display_name ]] Not set [[ member.email || '—' ]] [[ member.is_active ? 'Active' : 'Inactive' ]]
Settings
Project Settings
Loading...

These replace hardcoded values across all scripts and monitors. DB values take priority over ENV vars.

[[ s.description ]] ENV [[ s.env_fallback ]]: [[ projectSettings.envValues[s.key] ]] ENV [[ s.env_fallback ]]: not set
Project settings saved.
[[ projectSettings.saveError ]]
Slack Channel Assignments
Configure which Slack channel each type of report or alert is posted to

Loading channels...

[[ settings.error ]]
Could not load Slack channels: [[ settings.slackError ]]. Channel names will not auto-fill but saved IDs will still be used.
Slack Channel Assignments
Report / Alert Type Channel Env var fallback
[[ rt.label ]]
#[[ channelName(settings.assignments[rt.report_type]) ]]
[[ rt.env_fallback ]] current: [[ settings.envValues[rt.report_type] ]] not set
Channel assignments saved.
Sprint Pulse
Last run [[ sp.lastUpdated.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) ]]
[[ sp.error ]]

No sprint data loaded yet.

Fetching sprint data from Jira...

How scores are calculated:
ticket_score = (status_weight × 0.6) + (hours_logged / estimate × 0.4)
sprint_health = Σ(ticket_score × estimate) / Σ(estimate)
Status weights for this project:
[[ s ]]: [[ w ]]
[[ spHealthScore ]] /100
Sprint Pulse
[[ spGrade.label ]]

[[ spBannerExplanation ]]

[[ sp.data.sprint.name ]]
[[ spFmtD(sp.data.sprint.start_date) ]] – [[ spFmtD(sp.data.sprint.end_date) ]]
[[ spDaysLeft ]] business days left
[[ seg.label ]] [[ seg.tickets.length ]]t · [[ Math.round(seg.estHours) ]]h
[[ sp.drillDown ? 'Hide breakdown' : 'Why this score?' ]]
Score Waterfall — Each ticket's share of the [[ spHealthScore ]]/100 health score (weighted by estimate hours × status readiness)
[[ seg.label ]] [[ seg.tickets.length ]] ticket[[ seg.tickets.length !== 1 ? 's' : '' ]] · [[ Math.round(seg.estHours) ]]h est +[[ seg.pct ]]%
[[ t.key ]]
[[ t.summary ]] [[ t.estimate_hours || 4 ]]h +[[ spTicketContrib(t) ]]%
Each ticket's score = (status_weight × 0.6) + (hours_logged/estimate × 0.4). Contribution = score × estimate / total_estimate. Bigger tickets weigh more.
Sprint Burndown [[ spBurndownChart.left.isBehind ? 'Behind' : 'On Track' ]] [[ sp.burndown.data.complete_pct ]]% complete  ·  [[ sp.burndown.data.behind_hours > 0 ? '+' + sp.burndown.data.behind_hours + 'h behind' : Math.abs(sp.burndown.data.behind_hours) + 'h ahead' ]]

Historical burndown requires fetching Jira changelogs for each ticket.

May take 15–30s for large sprints.

Fetching changelogs from Jira... (~30s for large sprints)

[[ sp.burndown.error ]]
Remaining vs. Ideal
Are we burning down fast enough?
- - Ideal — Remaining
[[ lbl.h ]]h [[ lbl.label ]]
[[ spFmtD(sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].date) ]]
Remaining [[ sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].remaining ]]h
Ideal [[ Math.round((sp.burndown.data.initial_hours || sp.burndown.data.total_hours) * Math.max(0, 1 - sp.burndown.hoveredIdx / Math.max(sp.burndown.data.daily_snapshots.length - 1, 1))) ]]h
● Shipped [[ sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].complete ]]h
Risk Signals
Blocked · QA & review · in progress
— Blocked — QA / Review — In Progress - - Scope added
[[ lbl.h ]]h [[ lbl.label ]]
[[ spFmtD(sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].date) ]]
● Blocked [[ sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].blocked ]]h
● QA / Review [[ sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].verifying ]]h
● In Progress [[ sp.burndown.data.daily_snapshots[sp.burndown.hoveredIdx].active ]]h
● Scope added [[ spBurndownChart.right.scopeAddedVals[sp.burndown.hoveredIdx] ]]h
[[ sp.burndown.data.behind_hours > 0 ? '+' + sp.burndown.data.behind_hours + 'h behind' : Math.abs(sp.burndown.data.behind_hours) + 'h ahead' ]]
vs. ideal burndown at this point in the sprint
[[ sp.burndown.data.blocked_hours ]]h blocked
[[ Math.round(sp.burndown.data.blocked_hours / sp.burndown.data.total_hours * 100) ]]% of total sprint hours stuck
[[ sp.burndown.data.complete_hours ]]h shipped
[[ sp.burndown.data.complete_pct ]]% of total sprint work completed
+[[ sp.burndown.data.added_hours ]]h scope added
[[ sp.burndown.data.added_tickets ]] ticket[[ sp.burndown.data.added_tickets === 1 ? '' : 's' ]] added after sprint started — explains part of the gap
[[ activeAuditBlock.title ]]
[[ activeAuditBlock.subtitle ]]
[[ activeAuditBlock.formula ]]
[[ col.label ]]
[[ activeAuditBlock.totals[col.key] ?? '' ]]
Why We're Behind
[[ r.text ]]
Action Required [[ spActionItems.filter(a => a.severity === 'critical').length ]] critical
No immediate actions needed
[[ item.key ]] [[ item.summary ]] [[ item.status ]]
[[ item.assignee ]] — [[ item.reason ]]
Team Capacity
Tickets sorted by priority order. The ── Sprint Capacity Line ── marks where each person runs out of hours before sprint end. Tickets below it are at risk of not shipping.
[[ spInitials(dev.name) ]]
[[ dev.name ]] [[ spCapLabel(spDevMetrics(dev).capacityRatio).cls === 'danger' ? 'Overloaded' : spCapLabel(spDevMetrics(dev).capacityRatio).label ]] [[ spDevMetrics(dev).completedTickets ]]/[[ spDevMetrics(dev).totalTickets ]] done  •  [[ Math.round(spDevMetrics(dev).hoursRemaining) ]]h remaining / [[ Math.round(spDevMetrics(dev).availHours) ]]h available
No open tickets
Sprint Audit
All sprint metrics with full ticket-level evidence. Click any block to expand.
[[ block.title ]]
[[ block.subtitle ]]
[[ block.rows.length ]] rows
[[ block.formula ]]
[[ col.label ]]
[[ block.totals[col.key] ?? '' ]]
Jobs
Scheduled agent jobs — run manually or let them run on schedule
[[ formatCheckType(schedule.check_type) ]]

[[ schedule.description ]]

[[ schedule.schedule_expression ]]
Last run: [[ formatTimeAgo(schedule.last_run) ]] Never run
[[ jobResults[schedule.check_type].message ]] — [[ jobResults[schedule.check_type].violations_found ]] violations found
Sprint Planning
Capacity planning for upcoming sprints
Loading sprints… No future sprints found [[ planning.sprints.length ]] sprint[[ planning.sprints.length === 1 ? '' : 's' ]] available
[[ planning.error ]]
Loading sprint data…
Select a future sprint above to view capacity planning
Sprint Performance
Per-developer progress vs. sprint pace — active sprint
[[ sp.data.sprint.name ]]
[[ spElapsedDays ]] of [[ spTotalBizDays ]] business days elapsed  ·  [[ Math.round(spSprintPctElapsed * 100) ]]% through sprint
Loading sprint data…
No active sprint data
No team members with dev/designer/tech_lead roles found. Set roles in Team → Members.
Sprint progress [[ Math.round(spSprintPctElapsed * 100) ]]% elapsed
[[ sp.data.sprint.start_date || '—' ]] [[ sp.data.sprint.end_date || '—' ]]
Based on estimates only. The black marker shows ideal pace (sprint % elapsed). Bar segments: ■ Done · ■ In QA/Review/Pending (no further dev action needed) · ■ In Progress. Timesheet integration coming — will add actual hours billed vs. estimated.
[[ group.label ]]
[[ dev.display_name ]]
[[ dev.role === 'tech_lead' ? 'Tech Lead' : (dev.role === 'wa' ? 'Designer' : 'Dev') ]]
[[ dev.velocity_label ]]
Completion vs ideal pace
+[[ dev.near_done_tickets ]] in QA/review │ ideal [[ dev.ideal_done_pct ]]%  · 
No tickets assigned this sprint
[[ dev.done_tickets ]] / [[ dev.total_tickets ]]
Tickets done
[[ dev.near_done_tickets ]]
In QA / review
[[ dev.total_hours > 0 ? dev.total_hours + 'h' : dev.total_tickets + ' tix' ]] / [[ dev.total_hours > 0 ? dev.capacity_hours + 'h' : Math.round(dev.capacity_hours / 8) + 'd' ]]
Load / capacity
Actual hours billed: coming with timesheet integration
Agent Settings
Slack Channel Assignments

Configure which Slack channel each type of report or alert is posted to.

[[ settings.error ]]
Could not load Slack channels: [[ settings.slackError ]].
Report / Alert Type Channel Env var fallback
[[ rt.label ]]
#[[ channelName(settings.assignments[rt.report_type]) ]]
[[ rt.env_fallback ]] current: [[ settings.envValues[rt.report_type] ]] not set
Channel assignments saved.