diff --git a/.gitignore b/.gitignore
index 57491c1..067d3e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,5 @@ src/cocopp/**/*.dat
src/cocopp/**/*.tdat
src/cocopp/**/*.info
dist/
-ppdata/
\ No newline at end of file
+ppdata/
+venv/
\ No newline at end of file
diff --git a/README.md b/README.md
index 75f1780..12c4a80 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ The package uses data generated with the [COCO framework](https://coco-platform.
The main documentation pages for the `coco-postprocess` package `cocopp` can be found at
+
- [getting-started](https://numbbo.it/getting-started#postprocess)
- [API documentation](https://numbbo.github.io/coco-doc/apidocs/cocopp)
- [Issue tracker and bug reports](https://github.com/numbbo/coco-postprocess/issues)
diff --git a/src/cocopp/html/generator.py b/src/cocopp/html/generator.py
new file mode 100644
index 0000000..4dc9f04
--- /dev/null
+++ b/src/cocopp/html/generator.py
@@ -0,0 +1,87 @@
+"""HTML content generator for COCO post-processing."""
+
+import json
+import os
+
+class HtmlGenerator:
+ """Generates HTML content with dynamic JavaScript updates."""
+
+ STATIC_TEMPLATE = """
+
+
+
+
+
+ COCO Post-Processing Results
+
+
+
+
+COCO Post-Processing Results
+
+
+
+
+
+
+
+"""
+
+ def __init__(self):
+ pass
+
+ def generate_parent_index_data(self, algo_data, single_file_name, many_file_name):
+ """Generate data structure for parent index."""
+ return {
+ 'title': 'COCO Post-Processing Results',
+ 'header': 'COCO Post-Processing Results',
+ 'nav_links': [],
+ 'single': sorted(algo_data.get('single', [])),
+ 'comparison': sorted(algo_data.get('comparison', [])),
+ 'images': [],
+ 'single_file_name': single_file_name,
+ 'many_file_name': many_file_name
+ }
+
+ def generate_folder_content(self, current_dir, image_extension):
+ """Generate data structure for folder index."""
+ nav_links = [
+ 'Home',
+ 'Runtime profiles (with arrow keys navigation)',
+ 'Tables for selected targets',
+ 'Runtime profiles for selected targets'
+ ]
+
+ images = []
+ image_path = 'pprldmany-single-functions/pprldmany.%s' % image_extension
+ if os.path.isfile(os.path.join(current_dir, image_path)):
+ images.append(image_path)
+
+ return {
+ 'title': 'COCO Post-Processing Results',
+ 'header': 'Results Overview',
+ 'nav_links': nav_links,
+ 'single': [],
+ 'comparison': [],
+ 'images': images,
+ 'single_file_name': '',
+ 'many_file_name': ''
+ }
+
+ def render(self, data):
+ """Render HTML with injected data."""
+ json_data = json.dumps(data, ensure_ascii=False)
+ html = self.STATIC_TEMPLATE.replace('{data}', json_data)
+ return html
\ No newline at end of file
diff --git a/src/cocopp/html/index.py b/src/cocopp/html/index.py
new file mode 100644
index 0000000..df46701
--- /dev/null
+++ b/src/cocopp/html/index.py
@@ -0,0 +1,116 @@
+"""Main interface for generating folder index pages."""
+
+import logging
+import os
+from .generator import HtmlGenerator
+from .writer import HtmlWriter
+from .. import genericsettings
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+def collect_algorithm_data(directory):
+ """Collect algorithm data from directory structure."""
+ data = {'single': [], 'comparison': []}
+
+ if not os.path.isdir(directory):
+ return data
+
+ try:
+ for item in os.listdir(directory):
+ item_path = os.path.join(directory, item)
+ if not os.path.isdir(item_path):
+ continue
+
+ single_file = os.path.join(item_path, '%s.html' % genericsettings.single_algorithm_file_name)
+ if os.path.isfile(single_file):
+ data['single'].append(item)
+
+ many_file = os.path.join(item_path, '%s.html' % genericsettings.many_algorithm_file_name)
+ if os.path.isfile(many_file):
+ data['comparison'].append(item)
+ except OSError as e:
+ logger.warning("Error reading directory %s: %s" % (directory, str(e)))
+
+ return data
+
+def update_parent_index(parent_index_path):
+ """Update the parent index.html file with links to algorithm results.
+
+ This function only writes the HTML file once. Links are managed
+ dynamically via JavaScript based on available algorithm directories.
+
+ Args:
+ parent_index_path: Path to the parent index.html file
+
+ Raises:
+ IOError: If there are issues reading/writing the index file
+ """
+ try:
+ generator = HtmlGenerator()
+ parent_dir = os.path.dirname(os.path.realpath(parent_index_path))
+
+ # to collect data from available algorithms
+ algo_data = collect_algorithm_data(parent_dir)
+
+ # generating data structure for dynamic rendering
+ data = generator.generate_parent_index_data(
+ algo_data,
+ genericsettings.single_algorithm_file_name,
+ genericsettings.many_algorithm_file_name
+ )
+
+ # HTML rendering
+ html = generator.render(data)
+
+ # initial creation of the file (will be created only if it doesn't already exist)
+ writer = HtmlWriter()
+ if not os.path.isfile(parent_index_path):
+ writer.write_safely(parent_index_path, html)
+ logger.info("Created parent index at %s" % parent_index_path)
+ else:
+ logger.info("Parent index already exists at %s (using dynamic JS updates)" % parent_index_path)
+
+ except Exception as e:
+ logger.error("Failed to update parent index: %s" % str(e))
+ raise IOError("Failed to update parent index: %s" % str(e))
+
+def save_folder_index(filepath, image_extension):
+ """Generate and save a folder index file.
+
+ The HTML file is created once with static structure. Dynamic content
+ is managed via JavaScript and server-side data updates.
+
+ Args:
+ filepath: Path where the index file should be saved
+ image_extension: Extension for image files (e.g. 'svg', 'png')
+ """
+ if not filepath:
+ return
+
+ try:
+ # content data generation
+ generator = HtmlGenerator()
+ current_dir = os.path.dirname(os.path.realpath(filepath))
+ data = generator.generate_folder_content(current_dir, image_extension)
+
+ # rendering to HTML
+ html = generator.render(data)
+
+ # initial creation of the file (created only if it doesn't already exist)
+ writer = HtmlWriter()
+ if not os.path.isfile(filepath):
+ writer.write_safely(filepath, html)
+ logger.info("Created folder index at %s" % filepath)
+ else:
+ logger.info("Folder index already exists at %s (using dynamic JS updates)" % filepath)
+
+ # update parent index if needed
+ parent_dir = os.path.dirname(current_dir)
+ parent_index_path = os.path.join(parent_dir, 'index.html')
+ if not os.path.isfile(parent_index_path):
+ update_parent_index(parent_index_path)
+
+ except Exception as e:
+ logger.error("Failed to save folder index at %s: %s" % (filepath, str(e)))
+ raise
\ No newline at end of file
diff --git a/src/cocopp/html/renderer.js b/src/cocopp/html/renderer.js
new file mode 100644
index 0000000..18f3830
--- /dev/null
+++ b/src/cocopp/html/renderer.js
@@ -0,0 +1,57 @@
+// data injected by server
+var contentData = window.contentData || {};
+
+function renderLinks(data) {
+ var container = document.getElementById('linksContainer');
+ var html = '';
+
+ // Navigation links
+ if (data.nav_links && data.nav_links.length > 0) {
+ data.nav_links.forEach(function(link) {
+ if (link) {
+ html += '' + link + '
';
+ }
+ });
+ }
+
+ // comparison code
+ if (data.comparison && data.comparison.length > 0) {
+ html += 'Comparison Data
';
+ data.comparison.forEach(function(algo) {
+ var path = algo + '/' + data.many_file_name + '.html';
+ html += '';
+ });
+ }
+
+ // single algorithm code
+ if (data.single && data.single.length > 0) {
+ html += 'Single Algorithm Data
';
+ data.single.forEach(function(algo) {
+ var path = algo + '/' + data.single_file_name + '.html';
+ html += '';
+ });
+ }
+
+ container.innerHTML = html;
+}
+
+function renderImages(data) {
+ var container = document.getElementById('imagesContainer');
+ var html = '';
+
+ if (data.images && data.images.length > 0) {
+ data.images.forEach(function(img) {
+ html += '';
+ });
+ }
+
+ container.innerHTML = html;
+}
+
+// rendering code
+document.addEventListener('DOMContentLoaded', function() {
+ if (contentData) {
+ renderLinks(contentData);
+ renderImages(contentData);
+ }
+});
diff --git a/src/cocopp/html/writer.py b/src/cocopp/html/writer.py
new file mode 100644
index 0000000..fb41005
--- /dev/null
+++ b/src/cocopp/html/writer.py
@@ -0,0 +1,29 @@
+"""Safe file writing utilities for HTML output."""
+
+import os
+
+class HtmlWriter:
+ """Handles safe writing of HTML files with atomic operations."""
+
+ @staticmethod
+ def write_safely(filepath, content):
+ """Write content to file atomically using a temporary file.
+
+ Args:
+ filepath: Path to the output file
+ content: HTML content to write
+ """
+ filepath = str(filepath)
+
+ # creating parent directories (if they don't already exist)
+ parent_dir = os.path.dirname(filepath)
+ if parent_dir and not os.path.exists(parent_dir):
+ os.makedirs(parent_dir)
+
+ try:
+ with open(filepath, 'w', encoding='utf-8', newline='') as f:
+ f.write(content)
+ f.flush()
+ os.fsync(f.fileno()) # forcing writing to disk (for mac users)
+ except Exception as e:
+ raise IOError("Failed to write %s: %s" % (filepath, str(e)))
\ No newline at end of file
diff --git a/src/cocopp/ppfig.py b/src/cocopp/ppfig.py
index 5747684..fe76574 100644
--- a/src/cocopp/ppfig.py
+++ b/src/cocopp/ppfig.py
@@ -14,6 +14,8 @@
from matplotlib import pyplot as plt
import shutil
from six import advance_iterator
+from cocopp.html.index import save_folder_index
+
# from pdb import set_trace
# absolute_import => . refers to where ppfig resides in the package:
@@ -496,7 +498,7 @@ def save_single_functions_html(filename,
toolsdivers.replace_in_file(filename + add_to_names + '.html', '??COCOVERSION??', '
Data produced with COCO %s' % (toolsdivers.get_version_label(None)))
if parentFileName:
- save_folder_index_file(os.path.join(current_dir, parentFileName + '.html'), extension)
+ save_folder_index(os.path.join(current_dir, parentFileName + '.html'), extension)
def write_dimension_links(dimension, dimensions, index):
diff --git a/src/cocopp/refalgs/best2009-bbob.tar.gz.pickle b/src/cocopp/refalgs/best2009-bbob.tar.gz.pickle
new file mode 100644
index 0000000..33d68e1
Binary files /dev/null and b/src/cocopp/refalgs/best2009-bbob.tar.gz.pickle differ