Fix template rendering issue and improve export functionality

- Fix template rendering by storing rendered HTML in variable before writing
  This resolves subdirectories not appearing in navigation despite being
  correctly detected and included in template variables
- Add missing template variables (paths.work_dir) to gallery config
- Add CGI refresh handler for web-based gallery regeneration
- Improve PDF export functionality with better error handling
- Add ESC key shortcut documentation for exiting selection mode
- Enhanced export manager with proper dependency checking

Fixes issue where new subdirectories were detected but not displayed
in browser navigation due to incomplete template rendering.
This commit is contained in:
Kylian Schmidt
2025-07-20 08:59:10 +02:00
parent 2d98ed0e7a
commit dbd67f6039
5 changed files with 347 additions and 29 deletions
+161 -10
View File
@@ -214,21 +214,37 @@
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
} }
.export-commands code { .export-command-container {
background: var(--background-color); margin: 20px 0;
padding: 4px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
display: inline-block; border-radius: 8px;
margin-top: 8px; overflow: hidden;
}
.export-command {
background: var(--header-background);
padding: 16px;
border-bottom: 1px solid var(--border-color);
}
.export-command code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
color: var(--text-color);
word-break: break-all;
display: block;
background: none;
border: none;
padding: 0;
margin: 0;
} }
.export-actions { .export-actions {
display: flex; display: flex;
gap: 12px; gap: 8px;
justify-content: flex-end; padding: 12px 16px;
margin-top: 20px; background: var(--card-background);
} }
.export-actions button { .export-actions button {
@@ -254,6 +270,124 @@
.export-actions button:last-child:hover { .export-actions button:last-child:hover {
background: var(--primary-color-dark, #0056b3); background: var(--primary-color-dark, #0056b3);
} }
.copy-btn, .close-btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--card-background);
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
.copy-btn:hover {
background: var(--primary-color, #007bff);
color: white;
border-color: var(--primary-color, #007bff);
}
.close-btn {
background: #dc3545;
color: white;
border-color: #dc3545;
margin-left: auto;
}
.close-btn:hover {
background: #c82333;
border-color: #bd2130;
}
.export-details, .export-tips {
margin: 20px 0;
padding: 16px;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.export-details {
background: var(--header-background);
}
.export-tips {
background: var(--card-background);
border-color: var(--primary-color, #007bff);
border-left: 4px solid var(--primary-color, #007bff);
}
.export-details h4, .export-tips h4 {
margin: 0 0 12px 0;
color: var(--text-color);
font-size: 16px;
}
.export-details ul, .export-tips ul {
margin: 0;
padding-left: 20px;
color: var(--text-color);
}
.export-details li, .export-tips li {
margin: 8px 0;
line-height: 1.5;
}
.export-details code, .export-tips code {
background: var(--background-color);
padding: 2px 6px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
border: 1px solid var(--border-color);
}
.export-tips kbd {
background: var(--header-background);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 2px 6px;
font-family: inherit;
font-size: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.copy-feedback {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
z-index: 1003;
animation: fadeInOut 2s ease-in-out;
}
.copy-feedback-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.copy-feedback-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(-10px); }
20% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-10px); }
}
/* Dark theme adjustments */
[data-theme="dark"] .selection-checkbox { [data-theme="dark"] .selection-checkbox {
background: var(--card-background); background: var(--card-background);
border-color: var(--border-color); border-color: var(--border-color);
@@ -287,3 +421,20 @@
color: #f8d7da; color: #f8d7da;
border-color: #a94442; border-color: #a94442;
} }
[data-theme="dark"] .copy-feedback-success {
background: #155724;
color: #d4edda;
border-color: #1e7e34;
}
[data-theme="dark"] .copy-feedback-error {
background: #721c24;
color: #f8d7da;
border-color: #a94442;
}
[data-theme="dark"] .export-tips kbd {
background: var(--background-color);
color: var(--text-color);
}
+143 -15
View File
@@ -52,7 +52,7 @@ export class ExportManager {
this.toggleSelectionMode(); this.toggleSelectionMode();
} }
if (e.key === 'Escape') { if (e.key === 'Escape') {
this.clearSelection(); this.exitSelectionMode();
} }
}); });
@@ -118,10 +118,17 @@ export class ExportManager {
* Exit selection mode * Exit selection mode
*/ */
exitSelectionMode() { exitSelectionMode() {
const wasInSelectionMode = document.body.classList.contains('selection-mode');
document.body.classList.remove('selection-mode'); document.body.classList.remove('selection-mode');
document.getElementById('exportBtn').style.display = 'none'; document.getElementById('exportBtn').style.display = 'none';
document.getElementById('selectionCounter').style.display = 'none'; document.getElementById('selectionCounter').style.display = 'none';
this.clearSelection(); this.clearSelection();
// Show message if user was actually in selection mode
if (wasInSelectionMode) {
this.showMessage('Exited selection mode', 'info');
}
} }
/** /**
@@ -229,17 +236,26 @@ export class ExportManager {
return url; return url;
}); });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
const outputName = `merged_plots_${timestamp}.pdf`;
const payload = { const payload = {
plots: actualPaths, plots: actualPaths,
layout: this.calculateLayout(actualPaths.length), layout: this.calculateLayout(actualPaths.length),
output_name: `merged_plots_${new Date().toISOString().split('T')[0]}.pdf` output_name: outputName
}; };
// Generate a unique temporary filename
const tempFileName = `export_request_${Date.now()}.json`;
// Get work directory from config or fallback
const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web';
// Save the request to a JSON file that can be picked up by a Python script // Save the request to a JSON file that can be picked up by a Python script
const requestData = JSON.stringify(payload, null, 2); const requestData = JSON.stringify(payload, null, 2);
// Since we can't directly call Python from the browser, we'll show instructions // Show improved export instructions with full command
this.showExportInstructions(requestData); this.showExportInstructions(requestData, tempFileName, workDir);
} }
/** /**
@@ -258,21 +274,49 @@ export class ExportManager {
/** /**
* Show export instructions to user * Show export instructions to user
*/ */
showExportInstructions(requestData) { showExportInstructions(requestData, tempFileName, workDir) {
const tempFilePath = `/tmp/${tempFileName}`;
const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`;
const instructions = ` const instructions = `
<div class="export-instructions"> <div class="export-instructions">
<h3>Export Instructions</h3> <h3>🚀 Export Selected Plots</h3>
<p>To export your selected plots, save the following data to a file called <code>export_request.json</code> and run the export script:</p> <p>Run the following command in your terminal to export the selected plots:</p>
<div class="export-data">
<textarea readonly>${requestData}</textarea> <div class="export-command-container">
<div class="export-command">
<code id="exportCommand">${fullCommand}</code>
</div>
<div class="export-actions">
<button onclick="this.copyCommand()" class="copy-btn" title="Copy command to clipboard">
📋 Copy Command
</button>
<button onclick="this.copyJSON()" class="copy-btn" title="Copy JSON only">
📄 Copy JSON
</button>
<button onclick="this.close()" class="close-btn">
✕ Close
</button>
</div>
</div> </div>
<div class="export-commands">
<p><strong>Command to run:</strong></p> <div class="export-details">
<code>cd /work/kschmidt/web && python export_plots.py export_request.json</code> <h4>📋 Command Breakdown:</h4>
<ul>
<li><strong>Creates temporary file:</strong> <code>${tempFilePath}</code></li>
<li><strong>Changes to work directory:</strong> <code>${workDir}</code></li>
<li><strong>Runs export script:</strong> <code>python export_plots.py</code></li>
<li><strong>Output file:</strong> Will be saved in the work directory</li>
</ul>
</div> </div>
<div class="export-actions">
<button onclick="this.parentElement.parentElement.parentElement.remove()">Close</button> <div class="export-tips">
<button onclick="navigator.clipboard.writeText('${requestData.replace(/'/g, "\\'")}')">Copy JSON</button> <h4>💡 Tips:</h4>
<ul>
<li>The temporary JSON file will be automatically cleaned up after successful export</li>
<li>Use <kbd>Esc</kbd> to exit selection mode</li>
<li>Press <kbd>Ctrl+E</kbd> to toggle selection mode</li>
</ul>
</div> </div>
</div> </div>
`; `;
@@ -280,7 +324,59 @@ export class ExportManager {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'export-overlay'; overlay.className = 'export-overlay';
overlay.innerHTML = instructions; overlay.innerHTML = instructions;
// Add methods to the overlay for button handlers
overlay.copyCommand = function() {
navigator.clipboard.writeText(fullCommand).then(() => {
this.showCopyFeedback('Command copied to clipboard!');
}).catch(() => {
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
});
};
overlay.copyJSON = function() {
navigator.clipboard.writeText(requestData).then(() => {
this.showCopyFeedback('JSON copied to clipboard!');
}).catch(() => {
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
});
};
overlay.close = function() {
this.remove();
};
overlay.showCopyFeedback = function(message, type = 'success') {
const feedback = document.createElement('div');
feedback.className = `copy-feedback copy-feedback-${type}`;
feedback.textContent = message;
this.appendChild(feedback);
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 2000);
};
document.body.appendChild(overlay); document.body.appendChild(overlay);
// Close on ESC key
const handleEscape = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
// Close on clicking outside
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
document.removeEventListener('keydown', handleEscape);
}
});
} }
/** /**
@@ -318,3 +414,35 @@ export class ExportManager {
}, 3000); }, 3000);
} }
} }
// Add these methods to ExportManager if not present
ExportManager.prototype.isSelectionModeActive = function() {
return document.body.classList.contains('selection-mode');
};
ExportManager.prototype.exitSelectionMode = function() {
document.body.classList.remove('selection-mode');
if (typeof this.clearSelection === 'function') {
this.clearSelection();
}
};
// Ensure a single global instance
window.exportManager = window.exportManager || new ExportManager();
// Listen for ESC key globally to exit selection mode
// (This will work even if focus is not on a plot)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) {
window.exportManager.exitSelectionMode();
}
});
// Attach improved export logic to export button
document.addEventListener('DOMContentLoaded', function() {
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.addEventListener('click', function() {
window.exportManager.exportSelectedPlots();
});
}
});
+9 -1
View File
@@ -105,8 +105,16 @@ export class Utils {
refreshBtn.disabled = true; refreshBtn.disabled = true;
refreshBtn.textContent = '⏳'; refreshBtn.textContent = '⏳';
// Use the CGI script path from config if available
let cgiPath = '/cgi-bin/refresh_gallery.py';
if (window.galleryConfig && window.galleryConfig.paths && window.galleryConfig.paths.cgi_script) {
cgiPath = window.galleryConfig.paths.cgi_script;
// Ensure it starts with a slash for fetch
if (!cgiPath.startsWith('/')) cgiPath = '/' + cgiPath;
}
try { try {
const response = await fetch('/cgi-bin/refresh_gallery.py', { const response = await fetch(cgiPath, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
+29 -3
View File
@@ -15,6 +15,8 @@ Features:
import subprocess import subprocess
import shutil import shutil
import os
import sys
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@@ -114,6 +116,7 @@ def build_gallery(source_dir: Path, web_dir: Path,
current_metadata = merge_metadata(inherited_metadata, folder_metadata) current_metadata = merge_metadata(inherited_metadata, folder_metadata)
pdf_files = list(source_dir.glob("*.pdf")) pdf_files = list(source_dir.glob("*.pdf"))
subdirs = [d for d in source_dir.iterdir() if d.is_dir()] subdirs = [d for d in source_dir.iterdir() if d.is_dir()]
items = [] items = []
@@ -188,16 +191,18 @@ def build_gallery(source_dir: Path, web_dir: Path,
assets_path = "../" * (depth + 1) + "assets" assets_path = "../" * (depth + 1) + "assets"
with output_html.open("w") as f: with output_html.open("w") as f:
f.write(template.render( rendered_html = template.render(
title=title, title=title,
items=items, items=items,
subdirs=subdir_names, subdirs=subdir_names,
relpath=str(relative_path), relpath=str(relative_path),
paths=CONFIG.paths,
ui=CONFIG.ui, ui=CONFIG.ui,
stats=stats, stats=stats,
folder_metadata=current_metadata, folder_metadata=current_metadata,
assets_path=assets_path assets_path=assets_path
)) )
f.write(rendered_html)
print(f"Generated {output_html}") print(f"Generated {output_html}")
@@ -262,6 +267,21 @@ def format_file_size(size_bytes: int) -> str:
return f"{size:.1f} {size_names[i]}" return f"{size:.1f} {size_names[i]}"
def refresh_gallery_cgi():
"""
CGI handler to refresh the gallery from a web request.
Outputs a minimal HTTP response and triggers gallery regeneration.
"""
import traceback
print("Content-Type: text/plain\n")
try:
main()
print("Gallery refreshed successfully.")
except Exception as e:
print(f"Error refreshing gallery: {e}")
traceback.print_exc(file=sys.stdout)
def main() -> None: def main() -> None:
""" """
Main entry point for gallery generation. Main entry point for gallery generation.
@@ -356,6 +376,7 @@ def main() -> None:
items=items, items=items,
subdirs=[], subdirs=[],
relpath=source.name, relpath=source.name,
paths=CONFIG.paths,
ui=CONFIG.ui, ui=CONFIG.ui,
stats=stats, stats=stats,
folder_metadata={}, folder_metadata={},
@@ -375,7 +396,12 @@ def main() -> None:
f"directory nor a PDF file") f"directory nor a PDF file")
print("Done") print("Done")
# No copying of this script to CGI location
if __name__ == "__main__": if __name__ == "__main__":
main() # If run as CGI, call the CGI handler
if 'GATEWAY_INTERFACE' in os.environ:
refresh_gallery_cgi()
else:
main()
+5
View File
@@ -108,6 +108,10 @@
<span>Export plots</span> <span>Export plots</span>
<span class="shortcut-key">Ctrl+E</span> <span class="shortcut-key">Ctrl+E</span>
</div> </div>
<div class="shortcut-item">
<span>Exit selection mode</span>
<span class="shortcut-key">Esc</span>
</div>
<div class="shortcut-item"> <div class="shortcut-item">
<span>Refresh</span> <span>Refresh</span>
<span class="shortcut-key">F5</span> <span class="shortcut-key">F5</span>
@@ -186,6 +190,7 @@
window.galleryConfig = { window.galleryConfig = {
searchDebounceMs: {{ ui.search_debounce_ms|default(300) }}, searchDebounceMs: {{ ui.search_debounce_ms|default(300) }},
maxRecentPlots: {{ ui.max_recent_plots|default(20) }}, maxRecentPlots: {{ ui.max_recent_plots|default(20) }},
workDir: "{{ paths.work_dir }}",
stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %} stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %}
}; };
</script> </script>