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:
298
src/webserver.py
298
src/webserver.py
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user