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 os
import re
import asyncio import asyncio
import argparse import argparse
import logging import logging
from pathlib import Path from pathlib import Path
from urllib.parse import unquote from urllib.parse import unquote
from datetime import datetime from datetime import datetime, timedelta
from aiohttp import web from aiohttp import web
@@ -73,26 +74,84 @@ class SnapshotsWebServer:
content_type="text/html", 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): 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 = "" files_list = ""
# Soft accent colors for memory cards
accent_colors = ["#FFE4E1", "#E0F7FA", "#FFF8E1", "#F3E5F5", "#E8F5E9"]
if not html_files: if not html_files:
files_list = "<p class='no-files'>No snapshot files found.</p>" files_list = """
else: <div class="empty-state">
for file_info in html_files: <div class="empty-icon">📷</div>
size_mb = file_info["size"] / (1024 * 1024) <h3>No memories yet</h3>
files_list += f""" <p>Snapshots will appear here once they're downloaded</p>
<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>
</div> </div>
""" """
else:
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"""
<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""" return f"""
<!DOCTYPE html> <!DOCTYPE html>
@@ -100,7 +159,10 @@ class SnapshotsWebServer:
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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> <style>
* {{ * {{
margin: 0; margin: 0;
@@ -109,131 +171,169 @@ class SnapshotsWebServer:
}} }}
body {{ body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6; background: linear-gradient(135deg, #FFF5F5 0%, #FFF9E6 50%, #F0FFF4 100%);
color: #333; min-height: 100vh;
background-color: #f5f5f5; color: #4A4A4A;
padding: 20px;
}} }}
.container {{ .container {{
max-width: 1000px; max-width: 700px;
margin: 0 auto; margin: 0 auto;
padding: 40px 20px;
}} }}
.header {{ .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; 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 {{ .header h1 {{
color: #2c3e50; font-size: 2rem;
margin-bottom: 10px; font-weight: 700;
font-size: 2.5em; color: #2D3748;
margin-bottom: 8px;
}} }}
.header p {{ .header p {{
color: #7f8c8d; color: #718096;
font-size: 1.1em; font-size: 1.1rem;
}} }}
.files-container {{ .memories-count {{
display: inline-block;
background: white; background: white;
border-radius: 10px; padding: 8px 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-radius: 20px;
overflow: hidden; margin-top: 16px;
font-weight: 600;
color: #667EEA;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}} }}
.files-header {{ .memories-list {{
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;
display: flex; display: flex;
gap: 20px; flex-direction: column;
color: #7f8c8d; gap: 16px;
font-size: 0.9em;
}} }}
.no-files {{ .memory-card {{
padding: 40px; display: flex;
text-align: center; align-items: center;
color: #7f8c8d; gap: 16px;
font-size: 1.1em; 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 {{ .memory-card:hover {{
margin-top: 30px; 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; text-align: center;
color: #7f8c8d; padding: 60px 20px;
font-size: 0.9em; 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) {{ @media (max-width: 600px) {{
.file-meta {{ .container {{
flex-direction: column; padding: 24px 16px;
gap: 5px;
}} }}
.header h1 {{ .header h1 {{
font-size: 2em; font-size: 1.6rem;
}}
.memory-card {{
padding: 16px;
}}
.memory-title {{
font-size: 1rem;
}} }}
}} }}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <header class="header">
<h1>📸 ParentZone Snapshots</h1> <div class="header-icon">🌟</div>
<p>Browse and view your downloaded snapshot files</p> <h1>My Child's Memories</h1>
</div> <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="memories-list">
<div class="files-header">
📁 Available Snapshot Files ({len(html_files)} files)
</div>
{files_list} {files_list}
</div> </div>
<div class="footer">
<p>Served from: {self.snapshots_dir}</p>
<p>Server running on {self.host}:{self.port}</p>
</div>
</div> </div>
</body> </body>
</html> </html>