diff --git a/web/src/pages/evolution.ts b/web/src/pages/evolution.ts
index 2d7fc9c..5ad1b41 100644
--- a/web/src/pages/evolution.ts
+++ b/web/src/pages/evolution.ts
@@ -94,17 +94,17 @@ function renderDashboard(container: HTMLElement, data: EvolutionLiveData): void
-
+
Meta Tracker Best fitness per island over generations
-
+
Lineage Tree Program ancestry (top 80 by fitness)
-
+
@@ -509,9 +509,52 @@ function renderDashboard(container: HTMLElement, data: EvolutionLiveData): void
renderLiveStatus(document.getElementById('live-status')!, data.cycle);
renderStatistics(document.getElementById('statistics')!, data.totals);
renderActivityFeed(document.getElementById('activity-feed')!, data.recent_activity || []);
- renderMetaChart(document.getElementById('meta-chart')!, data.meta_snapshots ?? []);
- renderLineageTree(document.getElementById('lineage-tree')!, data.lineage ?? []);
- renderGenerationLog(document.getElementById('generation-log')!, data.generation_log ?? []);
+
+ // Below-the-fold sections: render immediately if already visible (polling update),
+ // otherwise defer with IntersectionObserver.
+ const belowFoldSections = container.querySelectorAll('.evo-below-fold');
+ const rendered = new Set();
+
+ function renderSection(name: string): void {
+ if (rendered.has(name)) return;
+ rendered.add(name);
+ switch (name) {
+ case 'meta':
+ renderMetaChart(document.getElementById('meta-chart')!, data.meta_snapshots ?? []);
+ break;
+ case 'lineage':
+ renderLineageTree(document.getElementById('lineage-tree')!, data.lineage ?? []);
+ break;
+ case 'genlog':
+ renderGenerationLog(document.getElementById('generation-log')!, data.generation_log ?? []);
+ break;
+ }
+ }
+
+ if (belowFoldSections.length > 0) {
+ const observer = new IntersectionObserver((entries) => {
+ for (const entry of entries) {
+ if (!entry.isIntersecting) continue;
+ const section = (entry.target as HTMLElement).dataset.evoSection;
+ if (section) {
+ renderSection(section);
+ observer.unobserve(entry.target);
+ }
+ }
+ }, { rootMargin: '200px' });
+
+ belowFoldSections.forEach(el => {
+ const name = el.dataset.evoSection;
+ if (!name) return;
+ // If already in viewport (e.g. polling update on small screen), render immediately
+ const rect = el.getBoundingClientRect();
+ if (rect.top < window.innerHeight + 200) {
+ renderSection(name);
+ } else {
+ observer.observe(el);
+ }
+ });
+ }
}
// ── Island Status ──────────────────────────────────────────────────────────────
@@ -932,36 +975,17 @@ function renderLineageTree(container: HTMLElement, nodes: LineageNode[]): void {
// ── Generation Log Table ───────────────────────────────────────────────────────
+const GEN_LOG_BATCH = 30;
+
function renderGenerationLog(container: HTMLElement, log: GenerationEntry[]): void {
if (!log || log.length === 0) {
container.innerHTML = 'No generation history yet.
';
return;
}
- const rows = log.map(e => {
- const color = ISLAND_COLORS[e.island] ?? '#94a3b8';
- const bestPct = (e.best_fitness * 100).toFixed(1);
- const avgPct = (e.avg_fitness * 100).toFixed(1);
- const barWidth = Math.round(e.best_fitness * 100);
- return `
-
- | ${e.generation} |
- ${escapeHtml(e.island)} |
- ${e.count} |
- ${e.promoted} |
-
-
- |
- ${avgPct}% |
- ${formatTimestamp(e.evaluated_at)} |
-
- `;
- });
+ // Show first batch immediately, paginate the rest
+ const initial = log.slice(0, GEN_LOG_BATCH);
+ const remaining = log.slice(GEN_LOG_BATCH);
container.innerHTML = `
@@ -976,11 +1000,64 @@ function renderGenerationLog(container: HTMLElement, log: GenerationEntry[]): vo
| Timestamp |
-
- ${rows.join('')}
+
+ ${initial.map(renderGenRow).join('')}
`;
+
+ if (remaining.length > 0) {
+ let offset = 0;
+ const total = remaining.length;
+ const btn = document.createElement('button');
+ btn.className = 'btn secondary show-more-btn';
+ btn.type = 'button';
+
+ function updateBtn(): void {
+ const left = total - offset;
+ if (left <= 0) { btn.remove(); return; }
+ const next = Math.min(GEN_LOG_BATCH, left);
+ btn.textContent = `Show ${next} more generations (${left} remaining)`;
+ btn.setAttribute('aria-label', `Show ${next} more generations, ${left} remaining`);
+ }
+
+ btn.addEventListener('click', () => {
+ const batch = remaining.slice(offset, offset + GEN_LOG_BATCH);
+ const tbody = document.getElementById('gen-log-tbody');
+ if (!tbody) return;
+ tbody.insertAdjacentHTML('beforeend', batch.map(renderGenRow).join(''));
+ offset += batch.length;
+ updateBtn();
+ });
+
+ updateBtn();
+ container.appendChild(btn);
+ }
+}
+
+function renderGenRow(e: GenerationEntry): string {
+ const color = ISLAND_COLORS[e.island] ?? '#94a3b8';
+ const bestPct = (e.best_fitness * 100).toFixed(1);
+ const avgPct = (e.avg_fitness * 100).toFixed(1);
+ const barWidth = Math.round(e.best_fitness * 100);
+ return `
+
+ | ${e.generation} |
+ ${escapeHtml(e.island)} |
+ ${e.count} |
+ ${e.promoted} |
+
+
+ |
+ ${avgPct}% |
+ ${formatTimestamp(e.evaluated_at)} |
+
+ `;
}
// ── Helpers ────────────────────────────────────────────────────────────────────