600600 margin-top : 24px ;
601601 }
602602
603- .efficiency-callout p {
603+ .efficiency-callout-success {
604+ background : linear-gradient (135deg , # e8f5e9 0% , # ffffff 100% );
605+ border : 2px solid # 4caf50 ;
606+ border-radius : 12px ;
607+ padding : 24px ;
608+ margin-top : 24px ;
609+ }
610+
611+ .efficiency-callout p ,
612+ .efficiency-callout-success p {
604613 margin : 0 0 16px 0 ;
605614 font-size : 16px ;
606615 line-height : 1.6 ;
607616 color : # 1d1d1f ;
608617 }
609618
610- .efficiency-callout p : last-child {
619+ .efficiency-callout p : last-child ,
620+ .efficiency-callout-success p : last-child {
611621 margin-bottom : 0 ;
612622 }
613623
614- .efficiency-callout strong {
624+ .efficiency-callout strong ,
625+ .efficiency-callout-success strong {
615626 color : # 000 ;
616627 font-weight : 600 ;
617628 }
618629
619- .efficiency-callout a {
630+ .efficiency-callout a ,
631+ .efficiency-callout-success a {
620632 color : # 007aff ;
621633 text-decoration : none;
622634 font-weight : 600 ;
623635 }
624636
625- .efficiency-callout a : hover {
637+ .efficiency-callout a : hover ,
638+ .efficiency-callout-success a : hover {
626639 text-decoration : underline;
627640 }
628641
698711 margin-bottom : 20px ;
699712 }
700713
701- .progress-header {
702- font-weight : 600 ;
703- margin-bottom : 12px ;
704- color : # 333 ;
705- }
706-
707- .progress-item {
708- padding : 8px 0 ;
709- font-size : 14px ;
714+ .progress-status {
715+ font-size : 15px ;
710716 color : # 555 ;
711- display : flex;
712- align-items : center;
713- gap : 8px ;
714- }
715-
716- .progress-item .status {
717717 font-weight : 500 ;
718- min-width : 80px ;
719718 }
720719
721- .progress-item .fetching .status { color : # 007aff ; }
722- .progress-item .processing .status { color : # ff9500 ; }
723- .progress-item .complete .status { color : # 34c759 ; }
724- .progress-item .error .status { color : # ff3b30 ; }
725-
726720 .progress-summary {
727721 margin-top : 12px ;
728722 padding-top : 12px ;
@@ -983,20 +977,25 @@ <h1>PR Cost Calculator</h1>
983977 < div class ="form-row ">
984978 < div class ="form-group ">
985979 < label for ="repoSampleSize "> Sample Size</ label >
986- < select id ="repoSampleSize ">
987- < option value ="25 " selected > 25 PRs (fast, ±20% accuracy)</ option >
988- < option value ="50 "> 50 PRs (slower, ±14% accuracy)</ option >
989- </ select >
990- < div class ="help-text "> Number of PRs to sample</ div >
980+ < input
981+ type ="number "
982+ id ="repoSampleSize "
983+ value ="25 "
984+ min ="1 "
985+ max ="50 "
986+ >
987+ < div class ="help-text "> 25 (fast, ±20% accuracy) or 50 (slower, ±14% accuracy)</ div >
991988 </ div >
992989 < div class ="form-group ">
993990 < label for ="repoDays "> Days Back</ label >
994- < select id ="repoDays ">
995- < option value ="10 "> 10 days</ option >
996- < option value ="30 "> 30 days</ option >
997- < option value ="90 " selected > 90 days</ option >
998- </ select >
999- < div class ="help-text "> Time period for analysis</ div >
991+ < input
992+ type ="number "
993+ id ="repoDays "
994+ value ="90 "
995+ min ="1 "
996+ max ="365 "
997+ >
998+ < div class ="help-text "> Number of days to look back</ div >
1000999 </ div >
10011000 </ div >
10021001 </ div >
@@ -1020,20 +1019,25 @@ <h1>PR Cost Calculator</h1>
10201019 < div class ="form-row ">
10211020 < div class ="form-group ">
10221021 < label for ="orgSampleSize "> Sample Size</ label >
1023- < select id ="orgSampleSize ">
1024- < option value ="25 " selected > 25 PRs (fast, ±20% accuracy)</ option >
1025- < option value ="50 "> 50 PRs (slower, ±14% accuracy)</ option >
1026- </ select >
1027- < div class ="help-text "> Number of PRs to sample</ div >
1022+ < input
1023+ type ="number "
1024+ id ="orgSampleSize "
1025+ value ="25 "
1026+ min ="1 "
1027+ max ="50 "
1028+ >
1029+ < div class ="help-text "> 25 (fast, ±20% accuracy) or 50 (slower, ±14% accuracy)</ div >
10281030 </ div >
10291031 < div class ="form-group ">
10301032 < label for ="orgDays "> Days Back</ label >
1031- < select id ="orgDays ">
1032- < option value ="10 "> 10 days</ option >
1033- < option value ="30 "> 30 days</ option >
1034- < option value ="90 " selected > 90 days</ option >
1035- </ select >
1036- < div class ="help-text "> Time period for analysis</ div >
1033+ < input
1034+ type ="number "
1035+ id ="orgDays "
1036+ value ="90 "
1037+ min ="1 "
1038+ max ="365 "
1039+ >
1040+ < div class ="help-text "> Number of days to look back</ div >
10371041 </ div >
10381042 </ div >
10391043 </ div >
@@ -1249,7 +1253,7 @@ <h3>Why calculate PR costs?</h3>
12491253 }
12501254 }
12511255
1252- function formatEfficiencyHTML ( efficiencyPct , grade , message , preventableCost , preventableHours , totalCost , totalHours , isAnnual = false , annualWasteCost = 0 , annualWasteHours = 0 , userCount = 0 , salary = 250000 , benefitsMultiplier = 1.2 , analysisType = 'project' , sourceName = '' ) {
1256+ function formatEfficiencyHTML ( efficiencyPct , grade , message , preventableCost , preventableHours , totalCost , totalHours , isAnnual = false , annualWasteCost = 0 , annualWasteHours = 0 , userCount = 0 , salary = 250000 , benefitsMultiplier = 1.2 , analysisType = 'project' , sourceName = '' , totalPRs = 0 , daysInPeriod = 90 ) {
12531257 let html = '<div class="efficiency-section">' ;
12541258 html += '<h2>Workflow Efficiency</h2>' ;
12551259
@@ -1277,42 +1281,35 @@ <h3>Why calculate PR costs?</h3>
12771281
12781282 html += '</div>' ; // Close efficiency-content
12791283
1280- // Callout for B or lower (not B+, not A-, not A, not A+)
1281- const showCallout = grade === 'B' || grade === 'B-' || grade === 'C+' || grade === 'C' ||
1282- grade === 'C-' || grade === 'D+' || grade === 'D' || grade === 'D-' || grade === 'F' ;
1283-
1284- if ( showCallout && isAnnual ) {
1285- // Calculate savings potential at 94% efficiency
1286- const totalAnnualCost = totalCost * ( annualWasteCost / preventableCost ) ;
1287- const targetWaste = totalAnnualCost * 0.06 ; // 6% waste at 94% efficiency
1288- const currentWaste = annualWasteCost ;
1289- let savings = currentWaste - targetWaste ;
1290-
1291- // Subtract our pricing: $5/month * 12 months * number of users
1292- const pricingCost = 5 * 12 * userCount ;
1293- savings -= pricingCost ;
1294-
1295- if ( savings > 0 ) {
1296- // Calculate average waste per PR
1297- const avgWasteHoursPerPR = extPreventableHours / e . total_prs ;
1298- const avgWasteCostPerPR = extPreventableCost / e . total_prs ;
1299-
1300- // Estimate PRs per author: total PRs in period / unique users in sample
1301- const daysInPeriod = parseInt ( days ) ;
1302- const prsPerPersonInPeriod = e . total_prs / userCount ;
1303- const prsPerPersonPerYear = prsPerPersonInPeriod * ( 365 / daysInPeriod ) ;
1304-
1305- // Calculate annual waste per author
1306- const annualWasteHoursPerAuthor = avgWasteHoursPerPR * prsPerPersonPerYear ;
1307- const annualWasteCostPerAuthor = avgWasteCostPerPR * prsPerPersonPerYear ;
1308-
1309- // Weekly waste per author
1310- const weeklyWastePerAuthor = annualWasteHoursPerAuthor / 52 ;
1311- const hoursPerWeek = weeklyWastePerAuthor . toFixed ( 1 ) ;
1284+ if ( isAnnual ) {
1285+ // Calculate per-author metrics for callout
1286+ const avgWasteHoursPerPR = preventableHours / totalPRs ;
1287+ const avgWasteCostPerPR = preventableCost / totalPRs ;
1288+ const prsPerPersonInPeriod = totalPRs / userCount ;
1289+ const prsPerPersonPerYear = prsPerPersonInPeriod * ( 365 / daysInPeriod ) ;
1290+ const annualWasteHoursPerAuthor = avgWasteHoursPerPR * prsPerPersonPerYear ;
1291+ const annualWasteCostPerAuthor = avgWasteCostPerPR * prsPerPersonPerYear ;
1292+ const weeklyWastePerAuthor = annualWasteHoursPerAuthor / 52 ;
1293+ const hoursPerWeek = weeklyWastePerAuthor . toFixed ( 1 ) ;
1294+
1295+ // Check if B+ or higher (A+, A, A-, B+)
1296+ const isHighPerformer = grade === 'A+' || grade === 'A' || grade === 'A-' || grade === 'B+' ;
1297+
1298+ // Check if B or lower
1299+ const showStrongCallout = grade === 'B' || grade === 'B-' || grade === 'C+' || grade === 'C' ||
1300+ grade === 'C-' || grade === 'D+' || grade === 'D' || grade === 'D-' || grade === 'F' ;
1301+
1302+ if ( isHighPerformer ) {
1303+ // Green callout for high performers
1304+ html += '<div class="efficiency-callout-success">' ;
1305+ html += `<p style="font-size: 16px; margin-bottom: 0; line-height: 1.5;">You probably don't need this as your devs only lose <strong>${ hoursPerWeek } hours/week due to pull request latency</strong>, but for everyone else, there is <a href="https://codegroove.dev/products/ready-to-review/" target="_blank">Ready-to-Review</a>. Free for OSS, $5/dev/month for private. Beta begins November 3rd: <a href="mailto:beta@codeGROOVE.dev">beta@codeGROOVE.dev</a></p>` ;
1306+ html += '</div>' ;
1307+ } else if ( showStrongCallout ) {
1308+ // Yellow callout for B or lower
13121309 const costPerPersonFormatted = '$' + Math . round ( annualWasteCostPerAuthor ) . toLocaleString ( 'en-US' ) ;
13131310
13141311 html += '<div class="efficiency-callout">' ;
1315- html += `<p style="font-size: 16px; margin-bottom: 16px; line-height: 1.5;">Your devs lose <strong>${ hoursPerWeek } hours/week</strong> waiting on code reviews . That's <strong>${ costPerPersonFormatted } /year per person</strong>. Ready-to-Review fixes this.</p>` ;
1312+ html += `<p style="font-size: 16px; margin-bottom: 16px; line-height: 1.5;">Your devs lose <strong>${ hoursPerWeek } hours/week due to pull request latency</strong> . That's <strong>${ costPerPersonFormatted } /year per person</strong>. <a href="https://codegroove.dev/products/ready-to-review/" target="_blank"> Ready-to-Review</a> fixes this.</p>` ;
13161313 html += '<p style="font-size: 16px; margin-bottom: 0; line-height: 1.5;">Free for OSS, $5/dev/month for private. Beta begins November 3rd: <a href="mailto:beta@codeGROOVE.dev">beta@codeGROOVE.dev</a></p>' ;
13171314 html += '</div>' ;
13181315 }
@@ -1754,7 +1751,6 @@ <h3>Why calculate PR costs?</h3>
17541751 // EventSource doesn't support POST, so we need a different approach
17551752 // We'll use fetch to initiate, but handle it as a proper SSE stream
17561753 return new Promise ( ( resolve , reject ) => {
1757- const progressMap = new Map ( ) ;
17581754 let progressContainer ;
17591755
17601756 // Make the POST request with fetch
@@ -1837,7 +1833,7 @@ <h3>Why calculate PR costs?</h3>
18371833 const salary = request . config ?. annual_salary || 250000 ;
18381834 const benefitsMultiplier = request . config ?. benefits_multiplier || 1.2 ;
18391835 const analysisType = mode === 'org' ? 'organization' : 'project' ;
1840- html += formatEfficiencyHTML ( extEfficiencyPct , extEfficiency . grade , extEfficiency . message , extPreventableCost , extPreventableHours , e . total_cost , e . total_hours , true , annualWasteCost , annualWasteHours , e . unique_users || 0 , salary , benefitsMultiplier , analysisType , sourceName ) ;
1836+ html += formatEfficiencyHTML ( extEfficiencyPct , extEfficiency . grade , extEfficiency . message , extPreventableCost , extPreventableHours , e . total_cost , e . total_hours , true , annualWasteCost , annualWasteHours , e . unique_users || 0 , salary , benefitsMultiplier , analysisType , sourceName , e . total_prs , parseInt ( days ) ) ;
18411837
18421838 // Calculate average PR efficiency
18431839 const totalPRs = e . total_prs ;
@@ -1871,36 +1867,29 @@ <h3>Why calculate PR costs?</h3>
18711867 return ;
18721868 }
18731869
1874- // Handle PR-specific progress
1870+ // Handle PR-specific progress - single line status update
18751871 if ( data . pr ) {
18761872 if ( ! progressContainer ) {
18771873 progressContainer = document . createElement ( 'div' ) ;
18781874 progressContainer . className = 'progress-container' ;
1879- progressContainer . innerHTML = '<div class="progress-header">Processing PRs... </div>' ;
1875+ progressContainer . innerHTML = '<div class="progress-status"> </div>' ;
18801876 resultDiv . innerHTML = '' ;
18811877 resultDiv . appendChild ( progressContainer ) ;
18821878 }
18831879
1884- let progressItem = progressMap . get ( data . pr ) ;
1885- if ( ! progressItem ) {
1886- progressItem = document . createElement ( 'div' ) ;
1887- progressItem . className = 'progress-item' ;
1888- progressContainer . appendChild ( progressItem ) ;
1889- progressMap . set ( data . pr , progressItem ) ;
1890- }
1891-
1880+ const statusLine = progressContainer . querySelector ( '.progress-status' ) ;
18921881 const repo = data . owner && data . repo ? `${ data . owner } /${ data . repo } ` : '' ;
1893- const prLink = repo ? `${ repo } #${ data . pr } ` : `#${ data . pr } ` ;
1882+ const prLink = repo ? `${ repo } #${ data . pr } ` : `PR #${ data . pr } ` ;
1883+ const progress = data . progress || '' ;
18941884
1895- progressItem . className = `progress-item ${ data . type } ` ;
18961885 if ( data . type === 'fetching' ) {
1897- progressItem . innerHTML = `<span class="status"> Fetching</span> ${ prLink } ${ data . progress || '' } ` ;
1886+ statusLine . innerHTML = `Fetching ${ prLink } ... ${ progress } ` ;
18981887 } else if ( data . type === 'processing' ) {
1899- progressItem . innerHTML = `<span class="status"> Processing</span> ${ prLink } ${ data . progress || '' } ` ;
1888+ statusLine . innerHTML = `Processing ${ prLink } ... ${ progress } ` ;
19001889 } else if ( data . type === 'complete' ) {
1901- progressItem . innerHTML = `<span class="status">✓ Complete</span> ${ prLink } ${ data . progress || '' } ` ;
1890+ statusLine . innerHTML = `✓ Completed ${ prLink } ${ progress } ` ;
19021891 } else if ( data . type === 'error' ) {
1903- progressItem . innerHTML = `<span class="status"> ✗ Error</span> ${ prLink } : ${ data . error } ` ;
1892+ statusLine . innerHTML = `✗ Error with ${ prLink } : ${ data . error } ` ;
19041893 }
19051894 }
19061895 } catch ( parseError ) {
0 commit comments