Skip to content

Commit 889d069

Browse files
authored
Merge pull request #36 from loryanstrant/copilot/fix-batch-files-extension
Fix batch file extensions for markdown output, add in-browser markdown rendering, and improve job details UX
2 parents 7bacf07 + 5122e61 commit 889d069

File tree

4 files changed

+441
-6
lines changed

4 files changed

+441
-6
lines changed

src/backup_analyzer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ def _process_batch(self, doc_type: str, files: List[str], output_dir: str) -> Li
397397
# Save batch documentation
398398
timestamp = datetime.now().isoformat()
399399
timestamp_safe = timestamp.replace(':', '-').replace('.', '-')
400-
batch_filename = f"batch_{doc_type}_{timestamp_safe}.html"
400+
file_ext = '.html' if self.config.OUTPUT_FORMAT.lower() == 'html' else '.md'
401+
batch_filename = f"batch_{doc_type}_{timestamp_safe}{file_ext}"
401402
batch_path = os.path.join(output_dir, batch_filename)
402403

403404
# Convert documentation to HTML if needed

src/web_server.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
"""
44
import os
55
import json
6+
import re
67
import logging
78
from datetime import datetime
89
import pytz
910
from pathlib import Path
1011
from typing import Dict, List, Optional
11-
from flask import Flask, render_template, jsonify, send_from_directory
12+
from flask import Flask, render_template, jsonify, send_from_directory, abort
13+
from werkzeug.security import safe_join
1214
from threading import Lock
1315

1416
from .config import Config
@@ -25,6 +27,29 @@ def _get_now_tz(config):
2527
return datetime.now()
2628

2729

30+
def _get_display_name(filename: str) -> str:
31+
"""Convert a filename like 'batch_users_2026-02-15T13-45-32-698994.md' to a human-readable name."""
32+
name = filename
33+
# Remove extension
34+
name = re.sub(r'\.(html|md)$', '', name)
35+
# Handle known special files
36+
if name == 'SUMMARY':
37+
return '📊 Summary Analysis'
38+
if name == 'INDEX':
39+
return '📑 Documentation Index'
40+
# Handle batch files: batch_<type>_<timestamp>
41+
match = re.match(r'^batch_([a-zA-Z]+(?:_[a-zA-Z]+)*)_(\d.+)$', name)
42+
if match:
43+
doc_type = match.group(1).replace('_', ' ').title()
44+
return f'📄 {doc_type} Configuration Batch'
45+
# Handle doc files: doc_<hash>
46+
match = re.match(r'^doc_([a-f0-9]+)$', name)
47+
if match:
48+
return f'📄 Document {match.group(1)}'
49+
# Fallback: clean up the name
50+
return f'📄 {name.replace("_", " ").title()}'
51+
52+
2853
class ProgressTracker:
2954
"""Thread-safe progress tracking for analysis jobs"""
3055

@@ -137,6 +162,7 @@ def get_job(job_id):
137162

138163
# Get output files if job is completed
139164
files = []
165+
summary_content = None
140166
if job['status'] == 'completed' and job.get('output_dir'):
141167
analysis_dir = os.path.join(job['output_dir'], 'analysis')
142168
if os.path.exists(analysis_dir):
@@ -145,11 +171,20 @@ def get_job(job_id):
145171
file_path = os.path.join(analysis_dir, file)
146172
files.append({
147173
'name': file,
174+
'display_name': _get_display_name(file),
148175
'size': os.path.getsize(file_path),
149176
'modified': datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
150177
})
178+
# Read summary file if it exists
179+
summary_path = os.path.join(analysis_dir, 'SUMMARY.md')
180+
if os.path.exists(summary_path):
181+
try:
182+
with open(summary_path, 'r', encoding='utf-8') as f:
183+
summary_content = f.read()
184+
except Exception as e:
185+
logger.warning(f"Failed to read summary file: {e}")
151186

152-
return render_template('job_details.html', job=job, files=files)
187+
return render_template('job_details.html', job=job, files=files, summary_content=summary_content)
153188

154189
@app.route('/api/job/<job_id>')
155190
def get_job_api(job_id):
@@ -180,13 +215,35 @@ def get_job_api(job_id):
180215

181216
@app.route('/view/<job_id>/<filename>')
182217
def view_file(job_id, filename):
183-
"""View analysis file"""
218+
"""View analysis file - renders markdown in the browser"""
184219
jobs = progress_tracker.get_jobs_history()
185220
job = next((j for j in jobs if j['id'] == job_id), None)
186221
if not job or not job.get('output_dir'):
187222
return "Job not found", 404
188223

189224
analysis_dir = os.path.join(job['output_dir'], 'analysis')
225+
file_path = safe_join(analysis_dir, filename)
226+
if file_path is None:
227+
abort(400)
228+
229+
if not os.path.isfile(file_path):
230+
return "File not found", 404
231+
232+
# Render markdown files in the browser instead of downloading
233+
if filename.endswith('.md'):
234+
try:
235+
with open(file_path, 'r', encoding='utf-8') as f:
236+
content = f.read()
237+
display_name = _get_display_name(filename)
238+
return render_template('markdown_view.html',
239+
content=content,
240+
filename=filename,
241+
display_name=display_name,
242+
job=job)
243+
except Exception as e:
244+
logger.warning(f"Failed to render markdown file: {e}")
245+
return send_from_directory(analysis_dir, filename)
246+
190247
return send_from_directory(analysis_dir, filename)
191248

192249
@app.route('/download/<job_id>/<filename>')

templates/job_details.html

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,52 @@
3636
.nav-link:hover {
3737
text-decoration: underline;
3838
}
39+
.summary-card {
40+
background: white;
41+
padding: 20px 30px;
42+
border-radius: 10px;
43+
margin-bottom: 20px;
44+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
45+
border-left: 4px solid #667eea;
46+
}
47+
.summary-card h2 {
48+
color: #333;
49+
margin-bottom: 10px;
50+
}
51+
.summary-actions {
52+
display: flex;
53+
gap: 10px;
54+
align-items: center;
55+
margin-bottom: 15px;
56+
}
57+
.summary-preview {
58+
background: #f8f9fa;
59+
padding: 15px 20px;
60+
border-radius: 8px;
61+
color: #333;
62+
line-height: 1.6;
63+
max-height: 300px;
64+
overflow-y: auto;
65+
}
66+
.summary-preview h1, .summary-preview h2, .summary-preview h3 {
67+
margin-top: 10px;
68+
margin-bottom: 5px;
69+
}
70+
.summary-preview table {
71+
width: 100%;
72+
border-collapse: collapse;
73+
margin: 10px 0;
74+
}
75+
.summary-preview th {
76+
background: #667eea;
77+
color: white;
78+
padding: 8px;
79+
text-align: left;
80+
}
81+
.summary-preview td {
82+
padding: 8px;
83+
border-bottom: 1px solid #e0e0e0;
84+
}
3985
.job-info {
4086
background: white;
4187
padding: 20px 30px;
@@ -96,9 +142,15 @@
96142
.file-info {
97143
flex: 1;
98144
}
99-
.file-name {
145+
.file-display-name {
100146
font-weight: 600;
101147
color: #333;
148+
margin-bottom: 3px;
149+
}
150+
.file-name {
151+
font-size: 0.8em;
152+
color: #888;
153+
font-family: 'Courier New', monospace;
102154
margin-bottom: 5px;
103155
}
104156
.file-meta {
@@ -132,6 +184,13 @@
132184
.btn-secondary:hover {
133185
background: #5a6268;
134186
}
187+
.btn-summary {
188+
background: #667eea;
189+
color: white;
190+
}
191+
.btn-summary:hover {
192+
background: #5568d3;
193+
}
135194
.status-badge {
136195
display: inline-block;
137196
padding: 5px 12px;
@@ -165,6 +224,17 @@ <h1>Job {{ job.id }} Details</h1>
165224
<a href="/" class="nav-link">← Back to Dashboard</a>
166225
</div>
167226

227+
{% if summary_content %}
228+
<div class="summary-card">
229+
<h2>📊 Summary Analysis</h2>
230+
<div class="summary-actions">
231+
<a href="/view/{{ job.id }}/SUMMARY.md" class="btn btn-summary">View Full Summary</a>
232+
<a href="/download/{{ job.id }}/SUMMARY.md" class="btn btn-secondary">⬇ Download</a>
233+
</div>
234+
<div class="summary-preview" id="summary-preview"></div>
235+
</div>
236+
{% endif %}
237+
168238
<div class="job-info">
169239
<h2>Job Information</h2>
170240
<div class="info-grid">
@@ -202,7 +272,8 @@ <h2>Generated Files ({{ files|length }})</h2>
202272
{% for file in files %}
203273
<div class="file-card">
204274
<div class="file-info">
205-
<div class="file-name">📄 {{ file.name }}</div>
275+
<div class="file-display-name">{{ file.display_name }}</div>
276+
<div class="file-name">{{ file.name }}</div>
206277
<div class="file-meta">
207278
Size: {{ "%.2f"|format(file.size / 1024) }} KB |
208279
Modified: {{ file.modified }}
@@ -226,5 +297,54 @@ <h2>Generated Files ({{ files|length }})</h2>
226297
{% endif %}
227298
</div>
228299
</div>
300+
301+
{% if summary_content %}
302+
<script>
303+
// Simple markdown to HTML renderer for summary preview
304+
function renderMarkdown(text) {
305+
let html = text
306+
.replace(/&/g, '&amp;')
307+
.replace(/</g, '&lt;')
308+
.replace(/>/g, '&gt;');
309+
html = html.replace(/^---[\s\S]*?---\n*/m, '');
310+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function(match, lang, code) {
311+
return '<pre><code>' + code.trimEnd() + '</code></pre>';
312+
});
313+
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
314+
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
315+
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
316+
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
317+
html = html.replace(/^---$/gm, '<hr>');
318+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
319+
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
320+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
321+
html = html.replace(/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)*)/gm, function(match, hdr, sep, body) {
322+
let hs = hdr.split('|').filter(c => c.trim());
323+
let rs = body.trim().split('\n');
324+
let t = '<table><thead><tr>';
325+
hs.forEach(h => t += '<th>' + h.trim() + '</th>');
326+
t += '</tr></thead><tbody>';
327+
rs.forEach(r => {
328+
let cs = r.split('|').filter(c => c.trim());
329+
t += '<tr>';
330+
cs.forEach(c => t += '<td>' + c.trim() + '</td>');
331+
t += '</tr>';
332+
});
333+
return t + '</tbody></table>';
334+
});
335+
html = html.replace(/^(\d+)\. (.+)$/gm, '<oli>$2</oli>');
336+
html = html.replace(/((?:<oli>.*<\/oli>\n?)+)/g, function(match) {
337+
return '<ol>' + match.replace(/<\/?oli>/g, function(t) { return t.replace('oli', 'li'); }) + '</ol>';
338+
});
339+
html = html.replace(/^[\s]*[-*] (.+)$/gm, '<li>$1</li>');
340+
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
341+
html = html.replace(/^(?!<[a-z]|$)(.+)$/gm, '<p>$1</p>');
342+
html = html.replace(/<p>\s*<\/p>/g, '');
343+
return html;
344+
}
345+
const summaryContent = {{ summary_content | tojson }};
346+
document.getElementById('summary-preview').innerHTML = renderMarkdown(summaryContent);
347+
</script>
348+
{% endif %}
229349
</body>
230350
</html>

0 commit comments

Comments
 (0)