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
This commit is contained in:
Kylian Schmidt
2025-08-07 16:00:18 +02:00
parent eb770428b9
commit 0a345a0f22
6 changed files with 229 additions and 89 deletions
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+18 -15
View File
@@ -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)
+204 -67
View File
@@ -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)
+5 -5
View File
@@ -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}")
View File