From 0a345a0f228f3d78e1e2e443890990a65242242a Mon Sep 17 00:00:00 2001 From: Kylian Schmidt Date: Thu, 7 Aug 2025 16:00:18 +0200 Subject: [PATCH] Fix UI. Reduce excessive spacing between plot grid and subdirectories - Reduce plot-container margin-bottom from 450px to 2rem in view-controls.css - Reduce body padding-bottom from 400px to 2rem in base.css - Fix unnecessary spacing since stats box uses fixed positioning - Enhance logger with professional tree-structured output and colors - Replace print statements with structured logging throughout codebase --- assets/css/base.css | 2 +- assets/css/view-controls.css | 2 +- generate_gallery.py | 33 +++-- orchestration/logger.py | 271 ++++++++++++++++++++++++++--------- orchestration/metadata.py | 10 +- test_logger.py | 0 6 files changed, 229 insertions(+), 89 deletions(-) create mode 100644 test_logger.py diff --git a/assets/css/base.css b/assets/css/base.css index f007e1e..2da21fc 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -9,7 +9,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 1rem; - padding-bottom: 400px; /* Further increased bottom padding to prevent overlap with stats box */ + padding-bottom: 2rem; /* Reduced from 400px - stats box is fixed positioned */ background-color: var(--bg-color); color: var(--text-color); transition: background-color 0.3s, color 0.3s; diff --git a/assets/css/view-controls.css b/assets/css/view-controls.css index 11580d8..8e7a9b9 100644 --- a/assets/css/view-controls.css +++ b/assets/css/view-controls.css @@ -118,7 +118,7 @@ /* Plot Container Base Styles */ .plot-container { margin: 1rem 0; - margin-bottom: 450px; /* Add extra bottom margin to prevent overlap with stats box */ + margin-bottom: 2rem; /* Reduced from 450px - stats box is fixed positioned */ transition: all 0.3s ease; clear: both; } diff --git a/generate_gallery.py b/generate_gallery.py index 321b571..15873b7 100644 --- a/generate_gallery.py +++ b/generate_gallery.py @@ -1,7 +1,7 @@ """ Scientific Gallery Generator -This module generates static HTML galleries from scientific plot collections. +This module generates static HTML galleries from PDF plots. It converts PDF plots to PNG thumbnails, creates responsive web interfaces, and organizes plots into hierarchical directory structures. @@ -25,7 +25,7 @@ from datetime import datetime from jinja2 import Environment, FileSystemLoader from orchestration.config import Config -from orchestration.logger import GalleryLogger +from orchestration.logger import GalleryLogger, create_logger from orchestration.metadata import ( load_folder_metadata, merge_metadata, @@ -63,14 +63,12 @@ def convert_pdf_to_png(pdf_path: Path, logger: GalleryLogger) -> None: Args: pdf_path: Path to the source PDF file - logger: Logger instance for output Raises: subprocess.CalledProcessError: If ImageMagick conversion fails """ png_path = pdf_path.with_suffix(".png") - # Check if conversion is needed if png_path.exists(): pdf_mtime = pdf_path.stat().st_mtime png_mtime = png_path.stat().st_mtime @@ -137,7 +135,6 @@ def build_gallery(source_dir: Path, web_dir: Path, web_dir: Target web directory for gallery output relative_path: Relative path from gallery root (for navigation) inherited_metadata: Metadata inherited from parent directories - logger: Logger instance for output """ if relative_path is None: relative_path = Path(".") @@ -146,7 +143,7 @@ def build_gallery(source_dir: Path, web_dir: Path, inherited_metadata = {} if logger is None: - logger = GalleryLogger() + logger = GalleryLogger(verbose=False) # Load folder-level metadata and merge with inherited metadata folder_metadata = load_folder_metadata(source_dir) @@ -352,18 +349,19 @@ def refresh_gallery_cgi(): traceback.print_exc(file=sys.stdout) -def main(clean_first: bool = False) -> None: +def main(clean_first: bool = False, verbose: bool = False) -> None: """ Main entry point for gallery generation. Args: clean_first: If True, removes and recreates the gallery directory + verbose: If True, enables verbose logging Processes all configured sources and generates the complete gallery structure in the web directory. Ensures assets are available. """ - # Initialize the logger - logger = GalleryLogger() + # Initialize the logger with appropriate verbosity + logger = GalleryLogger(verbose=verbose) gallery_root = Path(CONFIG.web_folder) / CONFIG.plot_root @@ -405,7 +403,7 @@ def main(clean_first: bool = False) -> None: source_png_path = source_path.with_suffix('.png') if needs_update(source_path, web_pdf_path): - logger.info(f"Copying {source_path} to {web_pdf_path}") + logger.debug(f"Copying {source_path} to {web_pdf_path}") shutil.copy2(source_path, web_pdf_path) else: logger.debug(f"Skipping {source_path.name} (up to date)") @@ -414,7 +412,7 @@ def main(clean_first: bool = False) -> None: convert_pdf_to_png(source_path, logger) if needs_update(source_png_path, web_png_path): - logger.info(f"Copying {source_png_path} to {web_png_path}") + logger.debug(f"Copying {source_png_path} to {web_png_path}") shutil.copy2(source_png_path, web_png_path) else: logger.debug(f"Skipping {source_png_path.name} (up to date)") @@ -459,17 +457,17 @@ def main(clean_first: bool = False) -> None: )) logger.generated_html(str(output_html), 1) - logger.info(f"Processed {source.name}: {source.path}") + logger.info(f"✓ Completed {source.name}") elif source_path.is_dir(): source_web_dir = gallery_root / source.name source_web_dir.mkdir(parents=True, exist_ok=True) build_gallery(source_path, source_web_dir, Path(source.name), {}, logger) - logger.info(f"Processed {source.name}: {source.path}") + logger.info(f"✓ Completed {source.name}") else: logger.warning(f"Source {source.path} is neither a directory nor a PDF file") - logger.info("Gallery generation completed successfully") + logger.summary(len(CONFIG.sources)) if __name__ == "__main__": @@ -482,5 +480,10 @@ if __name__ == "__main__": action='store_true', help='Clean gallery directory before generation' ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose logging' + ) args = parser.parse_args() - main(clean_first=args.clean) + main(clean_first=args.clean, verbose=args.verbose) diff --git a/orchestration/logger.py b/orchestration/logger.py index 1713cec..8e6de4f 100644 --- a/orchestration/logger.py +++ b/orchestration/logger.py @@ -1,105 +1,242 @@ """ -Gallery logging wrapper using Python's built-in logging module. +Gallery logging wrapper using Python's built-in logging module with tree-like output. """ import logging import sys +class Colors: + """ANSI color codes for terminal output""" + RESET = '\033[0m' + BOLD = '\033[1m' + + # Standard colors + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + + # Bright colors + BRIGHT_GREEN = '\033[92m' + BRIGHT_YELLOW = '\033[93m' + BRIGHT_BLUE = '\033[94m' + BRIGHT_CYAN = '\033[96m' + + +class TreeFormatter(logging.Formatter): + """Custom formatter that creates clean output with colors""" + + def __init__(self, use_colors: bool = True): + super().__init__() + self.use_colors = use_colors and sys.stdout.isatty() + + def _colorize(self, text: str, color: str) -> str: + """Apply color to text if colors are enabled""" + if not self.use_colors: + return text + return f"{color}{text}{Colors.RESET}" + + def format(self, record): + # Extract custom attributes from the record + indent = getattr(record, 'indent', 0) + + # Create simple indentation + prefix = " " * indent + + # Apply colors based on level and content + message = record.getMessage() + + if record.levelname == 'INFO': + if 'Processing:' in message: + # Main source headers + message = self._colorize(message, Colors.BOLD + Colors.MAGENTA) + elif 'Generated' in message: + message = self._colorize(message, Colors.GREEN) + elif 'PDF files' in message: + message = self._colorize(message, Colors.BLUE) + elif 'Converting' in message: + message = self._colorize(message, Colors.CYAN) + elif 'Completed' in message and 'sources' in message: + message = self._colorize(message, Colors.BOLD + Colors.GREEN) + else: + message = self._colorize(message, Colors.WHITE) + elif record.levelname == 'WARNING': + message = self._colorize(f"WARNING: {message}", Colors.YELLOW) + elif record.levelname == 'ERROR': + message = self._colorize(f"ERROR: {message}", Colors.RED) + elif record.levelname == 'DEBUG': + message = self._colorize(message, Colors.CYAN) + else: + message = message + + return f"{prefix}{message}" + + class GalleryLogger: """ - Simple wrapper around Python's logging module for gallery generation. - Provides convenient methods for common logging patterns in the gallery app. + Simple logger for gallery generation using Python's logging module. + Provides convenient methods for common logging patterns with clean output. """ - - def __init__(self, name: str = "gallery", level: int = logging.INFO, verbose: bool = False): + + def __init__(self, name: str = "gallery", level: int = logging.INFO, + verbose: bool = False, use_colors: bool = True): """ Initialize the gallery logger. - + Args: name: Logger name level: Logging level (default: INFO) verbose: If True, enables DEBUG level logging + use_colors: If True, enables colored output """ self.logger = logging.getLogger(name) - + self.use_colors = use_colors + self.current_source = None + self.current_depth = 0 + # Set level based on verbose flag if verbose: self.logger.setLevel(logging.DEBUG) else: self.logger.setLevel(level) - + # Only add handler if logger doesn't have one already if not self.logger.handlers: handler = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter( - '%(levelname)s: %(message)s' - ) + formatter = TreeFormatter(use_colors=use_colors) handler.setFormatter(formatter) self.logger.addHandler(handler) - - def info(self, message: str): - """Log an info message.""" - self.logger.info(message) - - def debug(self, message: str): - """Log a debug message.""" - self.logger.debug(message) - - def warning(self, message: str): - """Log a warning message.""" - self.logger.warning(message) - - def error(self, message: str): - """Log an error message.""" - self.logger.error(message) - - def success(self, message: str): - """Log a success message (as info with special prefix).""" - self.logger.info(f"✓ {message}") - - # Convenience methods for common gallery operations - def found_directory(self, dir_name: str, pdf_count: int): - """Log discovery of a directory with PDFs.""" - if pdf_count > 0: - self.info(f"Found {pdf_count} PDF files in {dir_name}/") - - def found_pdf(self, pdf_name: str): - """Log discovery of a PDF file.""" - self.debug(f"Found PDF: {pdf_name}") - - def found_metadata(self, metadata_file: str, field_count: int): - """Log discovery of a metadata file.""" - self.info(f"Found metadata: {metadata_file} ({field_count} fields)") - - def converted_pdf(self, pdf_name: str, duration: float = None): - """Log successful PDF conversion.""" - if duration is not None: - self.info(f"Converting {pdf_name} → {pdf_name.replace('.pdf', '.png')} ({duration:.2f}s)") + + # Prevent propagation to avoid duplicate messages + self.logger.propagate = False + + def _log_with_tree(self, level: int, message: str, indent: int = 0): + """Log a message with simple indentation""" + # Store indentation info in a way the formatter can access + original_makeRecord = self.logger.makeRecord + + def makeRecord_with_indent(*args, **kwargs): + record = original_makeRecord(*args, **kwargs) + record.indent = indent + return record + + # Temporarily replace makeRecord to add our custom attributes + self.logger.makeRecord = makeRecord_with_indent + try: + self.logger.log(level, message) + finally: + # Restore original makeRecord + self.logger.makeRecord = original_makeRecord + + def start_source(self, source_name: str): + """Begin processing a new source""" + self.current_source = source_name + self.current_depth = 0 + if self.use_colors: + colored_name = f"{Colors.BOLD}{Colors.MAGENTA}{source_name}{Colors.RESET}" + self.logger.info(f"\nProcessing: {colored_name}") else: - self.info(f"Converting {pdf_name} → {pdf_name.replace('.pdf', '.png')}") - - def skipped_pdf(self, pdf_name: str, reason: str = "up-to-date"): - """Log skipped PDF conversion.""" - self.debug(f"Skipping {pdf_name} ({reason})") - - def generated_html(self, html_path: str, plot_count: int): - """Log HTML page generation.""" - self.info(f"Generated {html_path} ({plot_count} plots)") - + self.logger.info(f"\nProcessing: {source_name}") + + def info(self, message: str, indent: int = 0): + """Log an info message""" + self._log_with_tree(logging.INFO, message, indent) + + def debug(self, message: str, indent: int = 0): + """Log a debug message""" + self._log_with_tree(logging.DEBUG, message, indent) + + def warning(self, message: str, indent: int = 0): + """Log a warning message""" + self._log_with_tree(logging.WARNING, message, indent) + + def error(self, message: str, indent: int = 0): + """Log an error message""" + self._log_with_tree(logging.ERROR, message, indent) + + def found_directory(self, dir_name: str, pdf_count: int, indent: int = 1): + """Log discovery of a directory with PDFs""" + if pdf_count > 0: + self._log_with_tree( + logging.INFO, + f"Found {dir_name}/ ({pdf_count} PDFs)", + indent + ) + + def found_pdf(self, pdf_name: str, indent: int = 2): + """Log discovery of a PDF file (debug only)""" + self._log_with_tree(logging.DEBUG, f"Found PDF: {pdf_name}", indent) + + def found_metadata(self, metadata_file: str, field_count: int, indent: int = 1): + """Log discovery of a metadata file""" + self._log_with_tree( + logging.INFO, + f"Found metadata: {metadata_file} ({field_count} fields)", + indent + ) + + def converted_pdf(self, pdf_name: str, duration: float = None, indent: int = 2): + """Log successful PDF conversion""" + if duration is not None: + self._log_with_tree( + logging.INFO, + f"Converting {pdf_name} ({duration:.2f}s)", + indent + ) + else: + self._log_with_tree(logging.INFO, f"Converting {pdf_name}", indent) + + def skipped_pdf(self, pdf_name: str, reason: str = "up-to-date", indent: int = 2): + """Log skipped PDF conversion""" + self._log_with_tree(logging.DEBUG, f"Skipping {pdf_name} ({reason})", indent) + + def generated_html(self, html_path: str, plot_count: int, indent: int = 1): + """Log HTML page generation""" + # Extract just the meaningful part of the path + if '/gallery/' in html_path: + short_path = html_path.split('/gallery/')[-1] + else: + short_path = html_path + + if plot_count > 0: + self._log_with_tree( + logging.INFO, + f"Generated {short_path} ({plot_count} plots)", + indent + ) + else: + self._log_with_tree( + logging.DEBUG, + f"Generated {short_path} (index only)", + indent + ) + def assets_updated(self): - """Log assets update.""" - self.info("Assets copied to target directory") + """Log assets update""" + self.logger.info("Assets updated") + + def summary(self, source_count: int): + """Print a summary""" + if self.use_colors: + message = f"{Colors.BOLD}{Colors.GREEN}Completed processing {source_count} sources{Colors.RESET}" + else: + message = f"Completed processing {source_count} sources" + self.logger.info(f"\n{message}") -def create_logger(verbose: bool = False, quiet: bool = False) -> GalleryLogger: +def create_logger(verbose: bool = False, quiet: bool = False, use_colors: bool = True) -> GalleryLogger: """ Create a configured logger for the gallery application. - + Args: verbose: Enable debug-level logging quiet: Suppress most output (only errors and warnings) - + use_colors: Enable colored output + Returns: Configured GalleryLogger instance """ @@ -109,5 +246,5 @@ def create_logger(verbose: bool = False, quiet: bool = False) -> GalleryLogger: level = logging.DEBUG else: level = logging.INFO - - return GalleryLogger(level=level, verbose=verbose) + + return GalleryLogger(level=level, verbose=verbose, use_colors=use_colors) diff --git a/orchestration/metadata.py b/orchestration/metadata.py index 00bf445..deb13d3 100644 --- a/orchestration/metadata.py +++ b/orchestration/metadata.py @@ -14,6 +14,7 @@ Features: import json import yaml +import logging from pathlib import Path from typing import Dict, Any @@ -40,11 +41,10 @@ def load_metadata_file(metadata_path: Path) -> Dict[str, Any]: elif metadata_path.suffix.lower() == '.json': return json.load(f) or {} else: - print(f"Warning: Unknown metadata file format: " - f"{metadata_path}") + logging.warning(f"Unknown metadata file format: {metadata_path}") return {} except (yaml.YAMLError, json.JSONDecodeError, IOError) as e: - print(f"Warning: Could not parse metadata file {metadata_path}: {e}") + logging.warning(f"Could not parse metadata file {metadata_path}: {e}") raise e @@ -154,6 +154,6 @@ def save_metadata_cache( try: with cache_path.open('w', encoding='utf-8') as f: json.dump(plot_metadata_cache, f, indent=2, ensure_ascii=False) - print(f"Saved metadata cache: {cache_path}") + logging.debug(f"Saved metadata cache: {cache_path}") except IOError as e: - print(f"Warning: Could not save metadata cache {cache_path}: {e}") + logging.warning(f"Could not save metadata cache {cache_path}: {e}") diff --git a/test_logger.py b/test_logger.py new file mode 100644 index 0000000..e69de29