Reuse per-algorithm Otava rendering in mix mode

Run mixed-series Otava analysis through the same compare helper, share the unified comparison-table renderer, and remove the legacy single-result shim.
diff --git a/src/otava_test_data/web/static/js/app.js b/src/otava_test_data/web/static/js/app.js
index d2af0dd..31dee82 100644
--- a/src/otava_test_data/web/static/js/app.js
+++ b/src/otava_test_data/web/static/js/app.js
@@ -1342,23 +1342,8 @@
                 console.error('Otava compare failed:', e);
                 data.otavaMulti = new Map();
             }
-            // Temporary back-compat: legacy single-otava consumers still read
-            // data.otava. Mirror the primary algo so the current chart, accuracy
-            // table, and detected table keep rendering until they migrate.
-            const primary = primaryOtavaAlgorithm();
-            const primaryRes = data.otavaMulti.get(primary);
-            if (primaryRes) {
-                data.otava = {
-                    detected_change_points: primaryRes.change_points || [],
-                    detected_indices: primaryRes.indices || [],
-                    count: primaryRes.count || 0,
-                };
-            } else {
-                data.otava = { detected_change_points: [], detected_indices: [], count: 0 };
-            }
         } else {
             data.otavaMulti = new Map();
-            data.otava = { detected_change_points: [], detected_indices: [], count: 0 };
         }
 
         updateChart(data);
@@ -1465,8 +1450,6 @@
     const groundTruthIndices = allChangePoints
         .filter(cp => cp.type !== 'outlier')
         .map(cp => cp.index);
-    const detectedIndices = data.otava?.detected_indices || [];
-
     // Run MA detection if enabled
     const runMa = runMaCheckbox.checked;
     const maWindow = parseInt(maWindowInput.value);
@@ -1474,9 +1457,6 @@
     const maResult = runMa ? detectChangePointsMA(values, maWindow, maThreshold) : { indices: [], details: [] };
     const maDetectedIndices = maResult.indices;
 
-    // Classify Otava detections
-    const otavaClassification = classifyDetections(detectedIndices, groundTruthIndices);
-
     // Classify MA detections
     const maClassification = classifyDetections(maDetectedIndices, groundTruthIndices);
 
@@ -3334,36 +3314,23 @@
             change_points: mixedChangePoints,
             count: mixedChangePoints.filter(cp => cp.type !== 'outlier').length
         },
-        otava: null,  // Will be computed by updateChart if checkbox is enabled
     };
 
-    // Run Otava analysis on mixed data if enabled
     if (isOtavaEnabled()) {
         try {
-            // The mix chart renders a single Otava result; pick the primary algo.
-            const algo = primaryOtavaAlgorithm();
-            const params = new URLSearchParams({
-                otava_algorithm: algo,
-                ...getOtavaParamsForAlgo(algo),
-            });
-            const response = await fetch(`/api/detect?${params}`, {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ data: mixedData })
-            });
-            const otavaResult = await response.json();
-            if (!otavaResult.error) {
-                fakeData.otava = otavaResult;
-            }
+            fakeData.otavaMulti = await runOtavaForEnabled(mixedData);
         } catch (error) {
             console.error('Failed to run Otava on mixed data:', error);
+            fakeData.otavaMulti = new Map();
         }
+    } else {
+        fakeData.otavaMulti = new Map();
     }
 
     updateChart(fakeData);
     updateStats(fakeData);
     updateAccuracyMetrics(fakeData);
-    updateMixComparisonTables(fakeData);
+    updateComparisonTables(fakeData);
     updateGeneratorInfo();
 
     // Show chart sections
@@ -3376,54 +3343,6 @@
 }
 
 /**
- * Update comparison tables for mix mode
- */
-function updateMixComparisonTables(data) {
-    truthTableBody.innerHTML = '';
-    detectedTableBody.innerHTML = '';
-
-    const groundTruth = data.ground_truth?.change_points || [];
-    const detected = data.otava?.detected_change_points || [];
-
-    // Ground truth table
-    if (groundTruth.length === 0) {
-        const row = document.createElement('tr');
-        row.innerHTML = '<td colspan="4" class="empty-message">No ground truth change points</td>';
-        truthTableBody.appendChild(row);
-    } else {
-        groundTruth.forEach(cp => {
-            const row = document.createElement('tr');
-            row.innerHTML = `
-                <td><strong>${cp.index}</strong></td>
-                <td>${cp.type}</td>
-                <td>${cp.description || '-'}</td>
-                <td>-</td>
-            `;
-            truthTableBody.appendChild(row);
-        });
-    }
-
-    // Detected table
-    if (!detected || detected.length === 0) {
-        const row = document.createElement('tr');
-        row.innerHTML = '<td colspan="5" class="empty-message">No change points detected by Otava</td>';
-        detectedTableBody.appendChild(row);
-    } else {
-        detected.forEach(cp => {
-            const row = document.createElement('tr');
-            row.innerHTML = `
-                <td><strong>${cp.index}</strong></td>
-                <td>${cp.mean_before?.toFixed(2) || '-'}</td>
-                <td>${cp.mean_after?.toFixed(2) || '-'}</td>
-                <td>${cp.pvalue?.toExponential(2) || '-'}</td>
-                <td>-</td>
-            `;
-            detectedTableBody.appendChild(row);
-        });
-    }
-}
-
-/**
  * Update the mix recipe display
  */
 function updateMixDisplay() {