@@ -23,8 +23,12 @@ let data = {
2323 sensitivity : null ,
2424 fills : null ,
2525 metricsHistory : null ,
26+ iterations : null ,
2627} ;
2728
29+ // Iterations state
30+ let filteredIterations = [ ] ;
31+
2832let currentView = 'overview' ;
2933let player = null ;
3034let playerUI = null ;
@@ -97,6 +101,17 @@ async function loadData() {
97101 console . log ( 'Metrics history not available (run "make metrics-history" to generate)' ) ;
98102 }
99103
104+ // Try to load iterations data (optional - may not exist yet)
105+ try {
106+ const iterationsRes = await fetch ( 'data/iterations.json' ) ;
107+ if ( iterationsRes . ok ) {
108+ data . iterations = await iterationsRes . json ( ) ;
109+ filteredIterations = data . iterations . iterations || [ ] ;
110+ }
111+ } catch ( e ) {
112+ console . log ( 'Iterations data not available' ) ;
113+ }
114+
100115 return true ;
101116 } catch ( err ) {
102117 console . error ( 'Error loading data:' , err ) ;
@@ -823,6 +838,283 @@ function renderFillsView() {
823838 gridContainer . innerHTML = fillConfigHtml + renderFillPatternGrid ( fillPatterns , selectedEntry . energy ) ;
824839}
825840
841+ // ============================================================================
842+ // Iterations View
843+ // ============================================================================
844+
845+ function renderIterationsView ( ) {
846+ // Render summary stats
847+ const summary = data . iterations ?. summary || { total : 0 , successful : 0 , failed : 0 , successRate : 0 } ;
848+ $ ( '#stat-total' ) . textContent = summary . total ;
849+ $ ( '#stat-success' ) . textContent = summary . successful ;
850+ $ ( '#stat-failed' ) . textContent = summary . failed ;
851+ $ ( '#stat-rate' ) . textContent = `${ ( summary . successRate * 100 ) . toFixed ( 1 ) } %` ;
852+
853+ // Render timeline
854+ renderIterationsTimeline ( ) ;
855+
856+ // Render iteration cards
857+ renderIterationCards ( ) ;
858+
859+ // Setup event listeners
860+ setupIterationsEventListeners ( ) ;
861+ }
862+
863+ function renderIterationsTimeline ( ) {
864+ const container = $ ( '#iterations-timeline-chart' ) ;
865+
866+ if ( ! data . metricsHistory || ! data . metricsHistory . timeline || data . metricsHistory . timeline . length < 2 ) {
867+ container . innerHTML = `
868+ <div class="no-timeline-data">
869+ <p>Not enough historical data to display timeline</p>
870+ <p class="hint">Run evaluations and tag baselines to track progress over time</p>
871+ </div>
872+ ` ;
873+ return ;
874+ }
875+
876+ container . innerHTML = renderTimelineChart ( data . metricsHistory , 900 , 280 ) ;
877+ }
878+
879+ function renderIterationCards ( ) {
880+ const container = $ ( '#iterations-list' ) ;
881+
882+ if ( ! filteredIterations || filteredIterations . length === 0 ) {
883+ container . innerHTML = `
884+ <div class="no-iterations">
885+ <p>No iterations found.</p>
886+ <p class="hint">Run <code>/iterate "goal"</code> to start hill-climbing.</p>
887+ </div>
888+ ` ;
889+ return ;
890+ }
891+
892+ const cards = filteredIterations . map ( createIterationCard ) . join ( '' ) ;
893+ container . innerHTML = cards ;
894+
895+ // Add event listeners to cards
896+ $$ ( '.card-header' ) . forEach ( header => {
897+ header . addEventListener ( 'click' , toggleIterationCardDetails ) ;
898+ } ) ;
899+
900+ $$ ( '.toggle-details' ) . forEach ( btn => {
901+ btn . addEventListener ( 'click' , ( e ) => {
902+ e . stopPropagation ( ) ;
903+ toggleIterationCardDetails . call ( btn . closest ( '.card-header' ) , e ) ;
904+ } ) ;
905+ } ) ;
906+ }
907+
908+ function createIterationCard ( iteration ) {
909+ const statusClass = iteration . status || 'unknown' ;
910+ const statusLabel = iteration . status || 'Unknown' ;
911+
912+ // Lever info
913+ const leverInfo = iteration . lever ? `
914+ <div class="card-lever">
915+ ${ iteration . lever . primary } : ${ iteration . lever . oldValue !== null ? iteration . lever . oldValue . toFixed ( 2 ) : '?' }
916+ → ${ iteration . lever . newValue !== null ? iteration . lever . newValue . toFixed ( 2 ) : '?' }
917+ ${ iteration . lever . delta ? `(${ iteration . lever . delta } )` : '' }
918+ </div>
919+ ` : '' ;
920+
921+ // Metrics info
922+ const metricsInfo = iteration . metrics ? `
923+ <div class="card-metrics">
924+ ${ iteration . metrics . target ? `
925+ <div class="metric-item">
926+ <div class="metric-label">Target Metric</div>
927+ <div class="metric-value ${ iteration . metrics . target . delta ?. startsWith ( '+' ) ? 'positive' : ( iteration . metrics . target . delta ?. startsWith ( '-' ) ? 'negative' : '' ) } ">
928+ ${ iteration . metrics . target . delta }
929+ </div>
930+ </div>
931+ ` : '' }
932+ ${ iteration . metrics . maxRegression ? `
933+ <div class="metric-item">
934+ <div class="metric-label">Max Regression</div>
935+ <div class="metric-value ${ iteration . metrics . maxRegression . startsWith ( '-' ) ? 'negative' : 'positive' } ">
936+ ${ iteration . metrics . maxRegression }
937+ </div>
938+ </div>
939+ ` : '' }
940+ </div>
941+ ` : '' ;
942+
943+ // Links
944+ const links = [ ] ;
945+ if ( iteration . pr && iteration . pr !== 'null' ) {
946+ links . push ( `<a href="https://github.com/chronick/duopulse/pull/${ iteration . pr . replace ( '#' , '' ) } " class="card-link" target="_blank">View PR ${ iteration . pr } </a>` ) ;
947+ }
948+ if ( iteration . commit && iteration . commit !== 'null' ) {
949+ links . push ( `<a href="https://github.com/chronick/duopulse/commit/${ iteration . commit } " class="card-link" target="_blank"><code>${ iteration . commit . substring ( 0 , 7 ) } </code></a>` ) ;
950+ }
951+ if ( iteration . branch && iteration . branch !== 'null' ) {
952+ links . push ( `<span class="card-link"><code>${ iteration . branch } </code></span>` ) ;
953+ }
954+
955+ return `
956+ <div class="iteration-card ${ statusClass } " id="${ iteration . id } ">
957+ <div class="card-header">
958+ <div class="card-title-row">
959+ <div class="card-id">#${ iteration . id } </div>
960+ <div class="card-status ${ statusClass } ">${ statusLabel } </div>
961+ </div>
962+ <h3 class="card-goal">${ escapeHTML ( iteration . goal ) } </h3>
963+ ${ leverInfo }
964+ ${ metricsInfo }
965+ ${ links . length > 0 ? `<div class="card-links">${ links . join ( '' ) } </div>` : '' }
966+ </div>
967+
968+ <div class="card-details" id="details-${ iteration . id } ">
969+ ${ createIterationCardDetails ( iteration ) }
970+ </div>
971+
972+ <button class="toggle-details">
973+ <span class="toggle-text">Show Details</span>
974+ <span class="toggle-icon">▼</span>
975+ </button>
976+ </div>
977+ ` ;
978+ }
979+
980+ function createIterationCardDetails ( iteration ) {
981+ const sections = [ ] ;
982+
983+ if ( iteration . sections ?. goal ) {
984+ sections . push ( `
985+ <div class="card-details-section">
986+ <h4>Goal</h4>
987+ <pre>${ escapeHTML ( iteration . sections . goal ) } </pre>
988+ </div>
989+ ` ) ;
990+ }
991+
992+ if ( iteration . sections ?. decision ) {
993+ sections . push ( `
994+ <div class="card-details-section">
995+ <h4>Decision</h4>
996+ <pre>${ escapeHTML ( iteration . sections . decision ) } </pre>
997+ </div>
998+ ` ) ;
999+ }
1000+
1001+ if ( iteration . sections ?. notes ) {
1002+ sections . push ( `
1003+ <div class="card-details-section">
1004+ <h4>Notes</h4>
1005+ <pre>${ escapeHTML ( iteration . sections . notes ) } </pre>
1006+ </div>
1007+ ` ) ;
1008+ }
1009+
1010+ if ( sections . length === 0 ) {
1011+ return '<p>No additional details available</p>' ;
1012+ }
1013+
1014+ return sections . join ( '' ) ;
1015+ }
1016+
1017+ function toggleIterationCardDetails ( e ) {
1018+ const card = e . currentTarget . closest ( '.iteration-card' ) ;
1019+ const details = card . querySelector ( '.card-details' ) ;
1020+ const toggleBtn = card . querySelector ( '.toggle-details' ) ;
1021+ const toggleText = toggleBtn . querySelector ( '.toggle-text' ) ;
1022+ const toggleIcon = toggleBtn . querySelector ( '.toggle-icon' ) ;
1023+
1024+ if ( details . classList . contains ( 'expanded' ) ) {
1025+ details . classList . remove ( 'expanded' ) ;
1026+ toggleText . textContent = 'Show Details' ;
1027+ toggleIcon . classList . remove ( 'expanded' ) ;
1028+ } else {
1029+ details . classList . add ( 'expanded' ) ;
1030+ toggleText . textContent = 'Hide Details' ;
1031+ toggleIcon . classList . add ( 'expanded' ) ;
1032+ }
1033+ }
1034+
1035+ function setupIterationsEventListeners ( ) {
1036+ const searchInput = $ ( '#search-iterations' ) ;
1037+ const filterStatus = $ ( '#filter-status' ) ;
1038+ const sortSelect = $ ( '#sort-iterations' ) ;
1039+
1040+ // Remove existing listeners by replacing elements (simple approach)
1041+ if ( searchInput && ! searchInput . dataset . listenerAttached ) {
1042+ searchInput . addEventListener ( 'input' , applyIterationsFilters ) ;
1043+ searchInput . dataset . listenerAttached = 'true' ;
1044+ }
1045+
1046+ if ( filterStatus && ! filterStatus . dataset . listenerAttached ) {
1047+ filterStatus . addEventListener ( 'change' , applyIterationsFilters ) ;
1048+ filterStatus . dataset . listenerAttached = 'true' ;
1049+ }
1050+
1051+ if ( sortSelect && ! sortSelect . dataset . listenerAttached ) {
1052+ sortSelect . addEventListener ( 'change' , applyIterationsSort ) ;
1053+ sortSelect . dataset . listenerAttached = 'true' ;
1054+ }
1055+ }
1056+
1057+ function applyIterationsFilters ( ) {
1058+ const searchTerm = ( $ ( '#search-iterations' ) ?. value || '' ) . toLowerCase ( ) ;
1059+ const statusFilter = $ ( '#filter-status' ) ?. value || 'all' ;
1060+
1061+ filteredIterations = ( data . iterations ?. iterations || [ ] ) . filter ( iteration => {
1062+ // Status filter
1063+ if ( statusFilter !== 'all' && iteration . status !== statusFilter ) {
1064+ return false ;
1065+ }
1066+
1067+ // Search filter
1068+ if ( searchTerm ) {
1069+ const searchableText = [
1070+ iteration . id ,
1071+ iteration . goal ,
1072+ iteration . lever ?. primary ,
1073+ iteration . sections ?. goal ,
1074+ iteration . sections ?. decision ,
1075+ iteration . sections ?. notes
1076+ ] . filter ( Boolean ) . join ( ' ' ) . toLowerCase ( ) ;
1077+
1078+ if ( ! searchableText . includes ( searchTerm ) ) {
1079+ return false ;
1080+ }
1081+ }
1082+
1083+ return true ;
1084+ } ) ;
1085+
1086+ applyIterationsSort ( ) ;
1087+ }
1088+
1089+ function applyIterationsSort ( ) {
1090+ const sortBy = $ ( '#sort-iterations' ) ?. value || 'newest' ;
1091+
1092+ switch ( sortBy ) {
1093+ case 'oldest' :
1094+ filteredIterations . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
1095+ break ;
1096+ case 'impact' :
1097+ filteredIterations . sort ( ( a , b ) => {
1098+ const deltaA = parseFloat ( a . metrics ?. target ?. delta ?. replace ( '%' , '' ) || '0' ) ;
1099+ const deltaB = parseFloat ( b . metrics ?. target ?. delta ?. replace ( '%' , '' ) || '0' ) ;
1100+ return deltaB - deltaA ;
1101+ } ) ;
1102+ break ;
1103+ case 'newest' :
1104+ default :
1105+ filteredIterations . sort ( ( a , b ) => b . id . localeCompare ( a . id ) ) ;
1106+ break ;
1107+ }
1108+
1109+ renderIterationCards ( ) ;
1110+ }
1111+
1112+ function escapeHTML ( text ) {
1113+ const div = document . createElement ( 'div' ) ;
1114+ div . textContent = text ;
1115+ return div . innerHTML ;
1116+ }
1117+
8261118// ============================================================================
8271119// Navigation
8281120// ============================================================================
@@ -841,7 +1133,7 @@ function showView(viewName) {
8411133 case 'sweeps' : renderSweepsView ( ) ; break ;
8421134 case 'seeds' : renderSeedsView ( ) ; break ;
8431135 case 'fills' : renderFillsView ( ) ; break ;
844- case 'sensitivity ' : renderSensitivityView ( ) ; break ;
1136+ case 'iterations ' : renderIterationsView ( ) ; break ;
8451137 }
8461138 }
8471139
@@ -865,7 +1157,7 @@ function showView(viewName) {
8651157
8661158function getViewFromHash ( ) {
8671159 const hash = window . location . hash . slice ( 1 ) ; // Remove the '#'
868- const validViews = [ 'overview' , 'presets' , 'sweeps' , 'seeds' , 'fills' , 'sensitivity ' ] ;
1160+ const validViews = [ 'overview' , 'presets' , 'sweeps' , 'seeds' , 'fills' , 'iterations ' ] ;
8691161 return validViews . includes ( hash ) ? hash : 'overview' ;
8701162}
8711163
0 commit comments