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:
+1
-1
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user