Reworked snapshot template output
All checks were successful
Build Docker Image / build (push) Successful in 2m28s
All checks were successful
Build Docker Image / build (push) Successful in 2m28s
This commit is contained in:
322
src/webserver.py
322
src/webserver.py
@@ -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,167 +74,266 @@ 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 = """
|
||||||
|
<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:
|
else:
|
||||||
for file_info in html_files:
|
for i, file_info in enumerate(html_files):
|
||||||
size_mb = file_info["size"] / (1024 * 1024)
|
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"""
|
files_list += f"""
|
||||||
<div class="file-item">
|
<a href="/{file_info["path"]}" class="memory-card" style="--accent: {accent}">
|
||||||
<div class="file-info">
|
<div class="memory-icon">📖</div>
|
||||||
<h3><a href="/{file_info["path"]}" class="file-link">{file_info["name"]}</a></h3>
|
<div class="memory-content">
|
||||||
<div class="file-meta">
|
<h3 class="memory-title">{date_range}</h3>
|
||||||
<span class="file-size">{size_mb:.2f} MB</span>
|
<p class="memory-time">Updated {time_ago}</p>
|
||||||
<span class="file-date">{file_info["modified"].strftime("%Y-%m-%d %H:%M:%S")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<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;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
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;
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.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;
|
text-align: center;
|
||||||
color: #7f8c8d;
|
padding: 60px 20px;
|
||||||
font-size: 1.1em;
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.footer {{
|
.empty-icon {{
|
||||||
margin-top: 30px;
|
font-size: 4rem;
|
||||||
text-align: center;
|
margin-bottom: 16px;
|
||||||
color: #7f8c8d;
|
opacity: 0.5;
|
||||||
font-size: 0.9em;
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
.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 ''}
|
||||||
<div class="files-container">
|
</header>
|
||||||
<div class="files-header">
|
|
||||||
📁 Available Snapshot Files ({len(html_files)} files)
|
<div class="memories-list">
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user