Initial commit — site configs

- vault.hidrago.click: Vaultwarden (password manager)
- git.hidrago.click: Gitea (git service)
- All static sites (suma, weather, combien-jour, tokyo-trip, etc.)
- Nginx configs for all subdomains
- Systemd user service for Vaultwarden
This commit is contained in:
2026-07-31 11:38:43 +01:00
commit 486d395e6b
28 changed files with 8053 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""Generate a static HTML page showing Louis and Maël's age in days and weeks (FR/EN/JA)."""
import json
from datetime import date, datetime, timezone
CHILDREN = [
{"name": "Louis", "birthdate": date(2018, 10, 14), "emoji": "👦", "color": "#667eea"},
{"name": "Maël", "birthdate": date(2020, 6, 24), "emoji": "🧒", "color": "#764ba2"},
]
TODAY = date.today()
# UI translations
I18N = {
"fr": {
"title": "Combien de jours ? — Louis & Maël",
"heading": "📅 Combien de jours ?",
"born": "né le {date}",
"days": "jours",
"weeks": "semaines",
"years": "ans",
"months": "mois",
"extra_days": "+ {n} j",
"updated": "Mis à jour le",
},
"en": {
"title": "How Many Days? — Louis & Maël",
"heading": "📅 How Many Days?",
"born": "born on {date}",
"days": "days",
"weeks": "weeks",
"years": "years",
"months": "months",
"extra_days": "+ {n} d",
"updated": "Updated on",
},
"ja": {
"title": "何日経った? — Louis & Maël",
"heading": "📅 何日経った?",
"born": "{date} 生まれ",
"days": "日",
"weeks": "週間",
"years": "歳",
"months": "ヶ月",
"extra_days": "+ {n} 日",
"updated": "更新日時",
},
}
# Month names for date formatting
MONTHS = {
"fr": ["janvier", "février", "mars", "avril", "mai", "juin",
"juillet", "août", "septembre", "octobre", "novembre", "décembre"],
"en": ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"],
"ja": ["1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"],
}
I18N_JSON = json.dumps(I18N, ensure_ascii=False)
MONTHS_JSON = json.dumps(MONTHS, ensure_ascii=False)
def calculate_age(birthdate):
delta = TODAY - birthdate
total_days = delta.days
weeks = total_days // 7
extra_days = total_days % 7
years = TODAY.year - birthdate.year
months = TODAY.month - birthdate.month
if TODAY.day < birthdate.day:
months -= 1
if months < 0:
years -= 1
months += 12
return {
"total_days": total_days,
"weeks": weeks,
"extra_days": extra_days,
"years": years,
"months": months,
}
def generate_html():
now = datetime.now(timezone.utc)
cards_html = []
for child in CHILDREN:
age = calculate_age(child["birthdate"])
birth_y = child["birthdate"].year
birth_m = child["birthdate"].month
birth_d = child["birthdate"].day
card = f"""
<div class="card" style="border-top: 5px solid {child['color']};">
<div class="card-header">
<h2>{child['emoji']} {child['name']}</h2>
<span class="birthdate" data-i18n="born" data-y="{birth_y}" data-m="{birth_m}" data-d="{birth_d}"></span>
</div>
<div class="age-grid">
<div class="age-item big">
<span class="number">{age['total_days']:,}</span>
<span class="unit" data-i18n="days">jours</span>
</div>
<div class="age-item big">
<span class="number">{age['weeks']:,}</span>
<span class="unit" data-i18n="weeks">semaines</span>
<span class="sub" data-i18n="extra_days" data-n="{age['extra_days']}"></span>
</div>
<div class="age-item small">
<span class="number">{age['years']}</span>
<span class="unit" data-i18n="years">ans</span>
</div>
<div class="age-item small">
<span class="number">{age['months']}</span>
<span class="unit" data-i18n="months">mois</span>
</div>
</div>
</div>"""
cards_html.append(card)
cards = "\n".join(cards_html)
html = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Combien de jours ? — Louis & Maël</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
}}
.lang-switch {{
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}}
.lang-btn {{
padding: 0.4rem 1rem;
border: 2px solid rgba(255,255,255,0.4);
background: rgba(255,255,255,0.15);
color: #fff;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s;
}}
.lang-btn:hover {{ background: rgba(255,255,255,0.3); }}
.lang-btn.active {{
background: #fff;
color: #764ba2;
border-color: #fff;
}}
h1 {{
color: #fff;
font-size: 2rem;
margin-bottom: 0.3rem;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}}
.subtitle {{
color: rgba(255,255,255,0.8);
font-size: 0.9rem;
margin-bottom: 2rem;
}}
.container {{
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: center;
max-width: 800px;
width: 100%;
}}
.card {{
background: rgba(255,255,255,0.95);
border-radius: 20px;
padding: 2rem;
width: 360px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
transition: transform 0.2s;
}}
.card:hover {{ transform: translateY(-4px); }}
.card-header {{
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1.5rem;
}}
.card-header h2 {{ font-size: 1.5rem; color: #333; }}
.birthdate {{ font-size: 0.8rem; color: #999; }}
.age-grid {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}}
.age-item {{
display: flex;
flex-direction: column;
align-items: center;
background: rgba(0,0,0,0.03);
border-radius: 12px;
padding: 1rem;
}}
.age-item.big {{ grid-column: span 2; }}
.age-item.small {{ }}
.number {{
font-size: 2.8rem;
font-weight: 800;
color: #333;
line-height: 1;
}}
.age-item.small .number {{ font-size: 1.8rem; }}
.unit {{
font-size: 0.85rem;
color: #888;
margin-top: 0.3rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.sub {{
font-size: 0.75rem;
color: #aaa;
margin-top: 0.15rem;
}}
footer {{
margin-top: 2rem;
color: rgba(255,255,255,0.6);
font-size: 0.8rem;
text-align: center;
}}
</style>
</head>
<body>
<div class="lang-switch">
<button class="lang-btn active" data-lang="fr">FR</button>
<button class="lang-btn" data-lang="en">EN</button>
<button class="lang-btn" data-lang="ja">日本語</button>
</div>
<h1 id="heading">📅 Combien de jours ?</h1>
<p class="subtitle" id="subtitle"></p>
<div class="container">
{cards}
</div>
<footer>
<span data-i18n="updated">Mis à jour le</span> {now.strftime('%d/%m/%Y à %H:%M UTC')}
</footer>
<script>
const I18N = {I18N_JSON};
const MONTHS = {MONTHS_JSON};
let currentLang = 'fr';
function formatDate(y, m, d, lang) {{
if (lang === 'fr') return d + ' ' + MONTHS[lang][m-1] + ' ' + y;
if (lang === 'en') return MONTHS[lang][m-1] + ' ' + d + ', ' + y;
if (lang === 'ja') return y + '年' + MONTHS[lang][m-1] + d + '日';
}}
function applyLang(lang) {{
currentLang = lang;
const t = I18N[lang];
document.title = t.title;
document.getElementById('heading').textContent = t.heading;
// Today's date as subtitle
const today = new Date();
document.getElementById('subtitle').textContent = formatDate(today.getFullYear(), today.getMonth()+1, today.getDate(), lang);
// Generic i18n labels
document.querySelectorAll('[data-i18n]').forEach(el => {{
const key = el.getAttribute('data-i18n');
if (!t[key]) return;
let text = t[key];
// Substitute date for born, n for extra_days
if (key === 'born') {{
const y = parseInt(el.getAttribute('data-y'));
const m = parseInt(el.getAttribute('data-m'));
const d = parseInt(el.getAttribute('data-d'));
text = text.replace('{{date}}', formatDate(y, m, d, lang));
}}
if (key === 'extra_days') {{
const n = el.getAttribute('data-n');
text = text.replace('{{n}}', n);
}}
el.textContent = text;
}});
// Update active button
document.querySelectorAll('.lang-btn').forEach(btn => {{
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
}});
document.documentElement.lang = lang;
}}
document.querySelectorAll('.lang-btn').forEach(btn => {{
btn.addEventListener('click', () => {{
applyLang(btn.getAttribute('data-lang'));
localStorage.setItem('age-lang', btn.getAttribute('data-lang'));
}});
}});
// Auto-detect
const saved = localStorage.getItem('age-lang');
if (saved && I18N[saved]) {{
applyLang(saved);
}} else {{
const browserLang = navigator.language.slice(0, 2);
if (I18N[browserLang]) applyLang(browserLang);
}}
</script>
</body>
</html>"""
return html
if __name__ == "__main__":
html = generate_html()
output_path = "/home/jerome/hermes-sites/combien-jour.hidrago.click/index.html"
with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
print(f"Age page generated at {output_path}")