Reworked snapshot template output
All checks were successful
Build Docker Image / build (push) Successful in 2m28s

This commit is contained in:
Tudor Sitaru
2026-01-05 10:40:41 +00:00
parent 04cf4c7db5
commit 14bc279136

View File

@@ -7,12 +7,13 @@ Provides a directory listing and serves static files from the snapshots folder.
"""
import os
import re
import asyncio
import argparse
import logging
from pathlib import Path
from urllib.parse import unquote
from datetime import datetime
from datetime import datetime, timedelta
from aiohttp import web
@@ -73,34 +74,95 @@ class SnapshotsWebServer:
content_type="text/html",
)
def _parse_date_range(self, filename):
"""Extract and format date range from filename for parent-friendly display."""
# Pattern: snapshots_YYYY-MM-DD_to_YYYY-MM-DD.html
match = re.match(r'snapshots_(\d{4}-\d{2}-\d{2})_to_(\d{4}-\d{2}-\d{2})\.html', filename)
if match:
try:
start = datetime.strptime(match.group(1), '%Y-%m-%d')
end = datetime.strptime(match.group(2), '%Y-%m-%d')
# Format nicely based on date range
if start.year == end.year and start.month == end.month:
# Same month: "January 5 - 12, 2024"
return f"{start.strftime('%B')} {start.day} - {end.day}, {start.year}"
elif start.year == end.year:
# Same year: "January 5 - February 12, 2024"
return f"{start.strftime('%B %d')} - {end.strftime('%B %d')}, {start.year}"
else:
# Different years: "December 28, 2023 - January 5, 2024"
return f"{start.strftime('%B %d, %Y')} - {end.strftime('%B %d, %Y')}"
except ValueError:
pass
# Fallback: clean up filename
return filename.replace('.html', '').replace('_', ' ').replace('snapshots ', '').title()
def _relative_time(self, dt):
"""Convert datetime to relative human-readable string."""
now = datetime.now()
diff = now - dt
if diff < timedelta(hours=1):
return "Just now"
elif diff < timedelta(hours=24):
hours = int(diff.total_seconds() / 3600)
return f"{hours} hour{'s' if hours > 1 else ''} ago"
elif diff < timedelta(days=7):
days = diff.days
if days == 1:
return "Yesterday"
return f"{days} days ago"
elif diff < timedelta(days=30):
weeks = diff.days // 7
return f"{weeks} week{'s' if weeks > 1 else ''} ago"
else:
return dt.strftime('%B %d, %Y')
def _generate_index_html(self, html_files):
"""Generate the HTML directory listing page."""
"""Generate the HTML directory listing page with parent-friendly design."""
files_list = ""
# Soft accent colors for memory cards
accent_colors = ["#FFE4E1", "#E0F7FA", "#FFF8E1", "#F3E5F5", "#E8F5E9"]
if not html_files:
files_list = "<p class='no-files'>No snapshot files found.</p>"
files_list = """
<div class="empty-state">
<div class="empty-icon">📷</div>
<h3>No memories yet</h3>
<p>Snapshots will appear here once they're downloaded</p>
</div>
"""
else:
for file_info in html_files:
size_mb = file_info["size"] / (1024 * 1024)
for i, file_info in enumerate(html_files):
date_range = self._parse_date_range(file_info["name"])
time_ago = self._relative_time(file_info["modified"])
accent = accent_colors[i % len(accent_colors)]
files_list += f"""
<div class="file-item">
<div class="file-info">
<h3><a href="/{file_info["path"]}" class="file-link">{file_info["name"]}</a></h3>
<div class="file-meta">
<span class="file-size">{size_mb:.2f} MB</span>
<span class="file-date">{file_info["modified"].strftime("%Y-%m-%d %H:%M:%S")}</span>
</div>
</div>
<a href="/{file_info["path"]}" class="memory-card" style="--accent: {accent}">
<div class="memory-icon">📖</div>
<div class="memory-content">
<h3 class="memory-title">{date_range}</h3>
<p class="memory-time">Updated {time_ago}</p>
</div>
<div class="memory-arrow">→</div>
</a>
"""
count_text = f'{len(html_files)} memory collection{"s" if len(html_files) != 1 else ""}' if html_files else ''
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ParentZone Snapshots</title>
<title>My Child's Memories</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* {{
margin: 0;
@@ -109,131 +171,169 @@ class SnapshotsWebServer:
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
padding: 20px;
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #FFF5F5 0%, #FFF9E6 50%, #F0FFF4 100%);
min-height: 100vh;
color: #4A4A4A;
}}
.container {{
max-width: 1000px;
max-width: 700px;
margin: 0 auto;
padding: 40px 20px;
}}
.header {{
background: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
margin-bottom: 40px;
}}
.header-icon {{
font-size: 4rem;
margin-bottom: 16px;
animation: gentle-bounce 3s ease-in-out infinite;
}}
@keyframes gentle-bounce {{
0%, 100% {{ transform: translateY(0); }}
50% {{ transform: translateY(-8px); }}
}}
.header h1 {{
color: #2c3e50;
margin-bottom: 10px;
font-size: 2.5em;
font-size: 2rem;
font-weight: 700;
color: #2D3748;
margin-bottom: 8px;
}}
.header p {{
color: #7f8c8d;
font-size: 1.1em;
color: #718096;
font-size: 1.1rem;
}}
.files-container {{
.memories-count {{
display: inline-block;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
padding: 8px 20px;
border-radius: 20px;
margin-top: 16px;
font-weight: 600;
color: #667EEA;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}}
.files-header {{
background: #3498db;
color: white;
padding: 20px;
font-size: 1.2em;
font-weight: bold;
}}
.file-item {{
border-bottom: 1px solid #ecf0f1;
padding: 20px;
transition: background-color 0.2s;
}}
.file-item:last-child {{
border-bottom: none;
}}
.file-item:hover {{
background-color: #f8f9fa;
}}
.file-link {{
color: #2c3e50;
text-decoration: none;
font-size: 1.1em;
font-weight: 500;
}}
.file-link:hover {{
color: #3498db;
text-decoration: underline;
}}
.file-meta {{
margin-top: 8px;
.memories-list {{
display: flex;
gap: 20px;
color: #7f8c8d;
font-size: 0.9em;
flex-direction: column;
gap: 16px;
}}
.no-files {{
padding: 40px;
text-align: center;
color: #7f8c8d;
font-size: 1.1em;
.memory-card {{
display: flex;
align-items: center;
gap: 16px;
background: white;
padding: 20px 24px;
border-radius: 16px;
text-decoration: none;
color: inherit;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
transition: all 0.2s ease;
border-left: 4px solid var(--accent, #FFE4E1);
}}
.footer {{
margin-top: 30px;
.memory-card:hover {{
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}}
.memory-icon {{
font-size: 2rem;
flex-shrink: 0;
}}
.memory-content {{
flex: 1;
min-width: 0;
}}
.memory-title {{
font-size: 1.15rem;
font-weight: 600;
color: #2D3748;
margin-bottom: 4px;
}}
.memory-time {{
font-size: 0.9rem;
color: #A0AEC0;
}}
.memory-arrow {{
font-size: 1.5rem;
color: #CBD5E0;
transition: transform 0.2s ease;
}}
.memory-card:hover .memory-arrow {{
transform: translateX(4px);
color: #667EEA;
}}
.empty-state {{
text-align: center;
color: #7f8c8d;
font-size: 0.9em;
padding: 60px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}}
.empty-icon {{
font-size: 4rem;
margin-bottom: 16px;
opacity: 0.5;
}}
.empty-state h3 {{
color: #4A5568;
margin-bottom: 8px;
}}
.empty-state p {{
color: #A0AEC0;
}}
@media (max-width: 600px) {{
.file-meta {{
flex-direction: column;
gap: 5px;
.container {{
padding: 24px 16px;
}}
.header h1 {{
font-size: 2em;
font-size: 1.6rem;
}}
.memory-card {{
padding: 16px;
}}
.memory-title {{
font-size: 1rem;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📸 ParentZone Snapshots</h1>
<p>Browse and view your downloaded snapshot files</p>
</div>
<header class="header">
<div class="header-icon">🌟</div>
<h1>My Child's Memories</h1>
<p>Precious moments from school</p>
{f'<div class="memories-count">📚 {count_text}</div>' if html_files else ''}
</header>
<div class="files-container">
<div class="files-header">
📁 Available Snapshot Files ({len(html_files)} files)
</div>
<div class="memories-list">
{files_list}
</div>
<div class="footer">
<p>Served from: {self.snapshots_dir}</p>
<p>Server running on {self.host}:{self.port}</p>
</div>
</div>
</body>
</html>