commit 486d395e6b8549fc07585ce52c860f8bc8b1888c Author: Jerome Backup Date: Fri Jul 31 11:38:43 2026 +0100 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea65964 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Ignore Docker data volumes (large, machine-specific) +vault.hidrago.click/vw-data/ +git.hidrago.click/gitea-data/ +git.hidrago.click/postgres-data/ + +# Node modules +node_modules/ \ No newline at end of file diff --git a/combien-jour.hidrago.click.nginx b/combien-jour.hidrago.click.nginx new file mode 100644 index 0000000..55b4580 --- /dev/null +++ b/combien-jour.hidrago.click.nginx @@ -0,0 +1,28 @@ +server { + server_name combien-jour.hidrago.click; + + root /home/jerome/hermes-sites/combien-jour.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + listen 443 ssl; # managed by Certbot + listen [::]:443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/combien-jour.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/combien-jour.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot +} + +server { + if ($host = combien-jour.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + server_name combien-jour.hidrago.click; + return 404; # managed by Certbot +} diff --git a/combien-jour.hidrago.click/generate_age.py b/combien-jour.hidrago.click/generate_age.py new file mode 100644 index 0000000..e09442d --- /dev/null +++ b/combien-jour.hidrago.click/generate_age.py @@ -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""" +
+
+

{child['emoji']} {child['name']}

+ +
+
+
+ {age['total_days']:,} + jours +
+
+ {age['weeks']:,} + semaines + +
+
+ {age['years']} + ans +
+
+ {age['months']} + mois +
+
+
""" + cards_html.append(card) + + cards = "\n".join(cards_html) + + html = f""" + + + + + Combien de jours ? — Louis & Maël + + + +
+ + + +
+

📅 Combien de jours ?

+

+
+{cards} +
+ + + + +""" + + 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}") diff --git a/combien-jour.hidrago.click/index.html b/combien-jour.hidrago.click/index.html new file mode 100644 index 0000000..28c8493 --- /dev/null +++ b/combien-jour.hidrago.click/index.html @@ -0,0 +1,248 @@ + + + + + + Combien de jours ? — Louis & Maël + + + +
+ + + +
+

📅 Combien de jours ?

+

+
+ +
+
+

👦 Louis

+ +
+
+
+ 2,847 + jours +
+
+ 406 + semaines + +
+
+ 7 + ans +
+
+ 9 + mois +
+
+
+ +
+
+

🧒 Maël

+ +
+
+
+ 2,228 + jours +
+
+ 318 + semaines + +
+
+ 6 + ans +
+
+ 1 + mois +
+
+
+
+ + + + + \ No newline at end of file diff --git a/espene-example.hidrago.click.nginx b/espene-example.hidrago.click.nginx new file mode 100644 index 0000000..6003cea --- /dev/null +++ b/espene-example.hidrago.click.nginx @@ -0,0 +1,40 @@ +server { + server_name espene-example.hidrago.click; + + root /home/jerome/hermes-sites/espene-example.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + location /ws { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/espene-example.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/espene-example.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +}server { + if ($host = espene-example.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + server_name espene-example.hidrago.click; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/espene-example.hidrago.click/espene-ws.service b/espene-example.hidrago.click/espene-ws.service new file mode 100644 index 0000000..f986369 --- /dev/null +++ b/espene-example.hidrago.click/espene-ws.service @@ -0,0 +1,15 @@ +[Unit] +Description=Espène Collaborative WebSocket Server +After=network.target + +[Service] +Type=simple +User=jerome +WorkingDirectory=/home/jerome/hermes-sites/espene-example.hidrago.click +ExecStart=/home/linuxbrew/.linuxbrew/bin/node /home/jerome/hermes-sites/espene-example.hidrago.click/server.mjs +Restart=always +RestartSec=3 +Environment=ESPENE_PORT=8080 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/espene-example.hidrago.click/index.html b/espene-example.hidrago.click/index.html new file mode 100644 index 0000000..6babcff --- /dev/null +++ b/espene-example.hidrago.click/index.html @@ -0,0 +1,1094 @@ + + + + + +Espène — dessin collaboratif + + + + + +
+
+
+

✏️ Espène

+ + +
+ + + + +
+ +
+ + +
+
+ + Page 1/1 + + +
+ + 100% + + +
+
+
+ +
+
+ +
+
+
+
🖊️
+
Utilisez un stylet
+
pour dessiner sur Espène
+
+
+
+
+ 📝 Texte reconnu + +
+
Appuyez sur 🔍
+
+
+
+ +
+ 🖊️ Stylet — + Hors ligne + — +
+ + + + + + + \ No newline at end of file diff --git a/espene-example.hidrago.click/package-lock.json b/espene-example.hidrago.click/package-lock.json new file mode 100644 index 0000000..7622ecb --- /dev/null +++ b/espene-example.hidrago.click/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "espene-example.hidrago.click", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "espene-example.hidrago.click", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ws": "^8.21.1" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/espene-example.hidrago.click/package.json b/espene-example.hidrago.click/package.json new file mode 100644 index 0000000..e83083c --- /dev/null +++ b/espene-example.hidrago.click/package.json @@ -0,0 +1,16 @@ +{ + "name": "espene-example.hidrago.click", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "ws": "^8.21.1" + } +} diff --git a/espene-example.hidrago.click/server.mjs b/espene-example.hidrago.click/server.mjs new file mode 100644 index 0000000..cf538e0 --- /dev/null +++ b/espene-example.hidrago.click/server.mjs @@ -0,0 +1,375 @@ +/** + * Espène — WebSocket collaborative server v2 + * Manages sessions with multiple pages, broadcasts strokes. + * + * Client → Server: + * { type: "create_session", name?: string } + * { type: "join", sessionId: string } + * { type: "leave" } + * { type: "list_sessions" } + * { type: "draw", action, x, y, color?, size?, isEraser? } + * { type: "undo" } + * { type: "clear" } + * { type: "switch_page", page: N } + * { type: "add_page" } + * { type: "delete_page", page: N } + * { type: "rename_page", page: N, name: string } + * { type: "pages_info" } + * + * Server → Client: + * { type: "sessions", sessions: [...] } + * { type: "session_joined", sessionId, clientId, pages: [...], currentPage } + * { type: "draw", ... } ← broadcast to other clients + * { type: "undo" } ← broadcast + * { type: "clear" } ← broadcast + * { type: "clients", count: N } + * { type: "page_switched", page, strokes, pageCount } + * { type: "page_added", page, pageCount } + * { type: "page_deleted", page, pageCount, currentPage } + * { type: "pages_info", pages: [...] } + * { type: "error", message } + */ + +import { WebSocketServer } from 'ws'; + +const PORT = parseInt(process.env.ESPENE_PORT || '8080', 10); +const MAX_STROKES_PER_PAGE = 2000; + +// ── Session store ──────────────────────────────────────────────── +// Map +// Session = { id, name, pages: Page[], clientIds: Set } +// Page = { name: string, strokes: Stroke[] } +const sessions = new Map(); + +function generateId() { + return Math.random().toString(36).slice(2, 8).toUpperCase(); +} + +function createSession(name) { + const id = generateId(); + sessions.set(id, { + id, + name: name || `Session ${id}`, + pages: [{ name: 'Page 1', strokes: [] }], + clientIds: new Set(), + }); + return id; +} + +// ── WebSocket server ───────────────────────────────────────────── +const wss = new WebSocketServer({ port: PORT }); +console.log(`[Espène WS v2] Server listening on port ${PORT}`); + +wss.on('connection', (ws) => { + const clientId = generateId(); + let currentSessionId = null; + let currentPage = 0; // per-client page + + function broadcastToSession(sessionId, message, exclude = null) { + const session = sessions.get(sessionId); + if (!session) return; + const raw = JSON.stringify(message); + wss.clients.forEach((client) => { + if (client.readyState === 1 && client._sessionId === sessionId && client !== exclude) { + client.send(raw); + } + }); + } + + function broadcastToAll(message) { + const raw = JSON.stringify(message); + wss.clients.forEach((client) => { + if (client.readyState === 1) client.send(raw); + }); + } + + function getSessionList() { + return Array.from(sessions.entries()) + .filter(([_, s]) => s.clientIds.size > 0 || s.pages.some(p => p.strokes.length > 0)) + .map(([id, s]) => ({ + id, name: s.name, + clientCount: s.clientIds.size, + pageCount: s.pages.length, + strokeCount: s.pages.reduce((sum, p) => sum + p.strokes.length, 0), + })); + } + + function broadcastSessionList() { + broadcastToAll({ type: 'sessions', sessions: getSessionList() }); + } + + function broadcastClientCount(sessionId) { + const session = sessions.get(sessionId); + if (!session) return; + broadcastToSession(sessionId, { type: 'clients', count: session.clientIds.size, clientId }); + } + + function getCurrentPageStrokes(session) { + if (!session || !session.pages[currentPage]) return []; + return session.pages[currentPage].strokes; + } + + ws.on('message', (raw) => { + let msg; + try { + msg = JSON.parse(raw.toString()); + } catch { + ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); + return; + } + + const session = currentSessionId ? sessions.get(currentSessionId) : null; + + switch (msg.type) { + + // ── List sessions ───────────────────────────────────────── + case 'list_sessions': { + ws.send(JSON.stringify({ type: 'sessions', sessions: getSessionList() })); + break; + } + + // ── Create session ───────────────────────────────────────── + case 'create_session': { + const sid = createSession(msg.name || null); + const s = sessions.get(sid); + ws.send(JSON.stringify({ type: 'session_created', sessionId: sid, name: s.name })); + broadcastSessionList(); + break; + } + + // ── Join session ─────────────────────────────────────────── + case 'join': { + const sid = msg.sessionId; + if (!sid || !sessions.has(sid)) { + ws.send(JSON.stringify({ type: 'error', message: `Session "${sid}" not found` })); + return; + } + const s = sessions.get(sid); + if (currentSessionId) { + const old = sessions.get(currentSessionId); + if (old) old.clientIds.delete(clientId); + broadcastClientCount(currentSessionId); + } + currentSessionId = sid; + currentPage = 0; + ws._sessionId = sid; + s.clientIds.add(clientId); + + ws.send(JSON.stringify({ + type: 'session_joined', + sessionId: sid, + clientId, + name: s.name, + pages: s.pages.map((p, i) => ({ index: i, name: p.name, strokeCount: p.strokes.length })), + currentPage: 0, + strokes: s.pages[0].strokes, + })); + broadcastClientCount(sid); + broadcastSessionList(); + break; + } + + // ── Leave session ────────────────────────────────────────── + case 'leave': { + if (currentSessionId) { + const s = sessions.get(currentSessionId); + if (s) s.clientIds.delete(clientId); + broadcastClientCount(currentSessionId); + broadcastSessionList(); + ws._sessionId = null; + currentSessionId = null; + } + ws.send(JSON.stringify({ type: 'left' })); + break; + } + + // ── Draw stroke ──────────────────────────────────────────── + case 'draw': { + if (!session) { + ws.send(JSON.stringify({ type: 'error', message: 'Not in a session' })); + return; + } + const page = session.pages[currentPage]; + if (!page) return; + + if (msg.action === 'start') { + session._pendingStroke = { + points: [{ x: msg.x, y: msg.y }], + color: msg.color || '#e94560', + size: msg.size || 3, + isEraser: msg.isEraser || false, + clientId, + id: generateId(), + page: currentPage, + }; + } else if (msg.action === 'move' && session._pendingStroke) { + session._pendingStroke.points.push({ x: msg.x, y: msg.y }); + } else if (msg.action === 'end' && session._pendingStroke) { + session._pendingStroke.points.push({ x: msg.x, y: msg.y }); + page.strokes.push(session._pendingStroke); + if (page.strokes.length > MAX_STROKES_PER_PAGE) { + page.strokes.splice(0, page.strokes.length - MAX_STROKES_PER_PAGE); + } + delete session._pendingStroke; + } + + broadcastToSession(currentSessionId, { + type: 'draw', + action: msg.action, + x: msg.x, y: msg.y, + color: msg.color, + size: msg.size, + isEraser: msg.isEraser, + clientId, + page: currentPage, + }, ws); + break; + } + + // ── Undo ─────────────────────────────────────────────────── + case 'undo': { + if (!session) return; + const page = session.pages[currentPage]; + if (!page || page.strokes.length === 0) return; + page.strokes.pop(); + broadcastToSession(currentSessionId, { type: 'undo', clientId, page: currentPage }, ws); + break; + } + + // ── Clear ────────────────────────────────────────────────── + case 'clear': { + if (!session) return; + const page = session.pages[currentPage]; + if (!page) return; + page.strokes = []; + broadcastToSession(currentSessionId, { type: 'clear', clientId, page: currentPage }, ws); + break; + } + + // ── Switch page ──────────────────────────────────────────── + case 'switch_page': { + if (!session) return; + const newPage = parseInt(msg.page, 10); + if (isNaN(newPage) || newPage < 0 || newPage >= session.pages.length) { + ws.send(JSON.stringify({ type: 'error', message: `Invalid page: ${newPage}` })); + return; + } + currentPage = newPage; + ws.send(JSON.stringify({ + type: 'page_switched', + page: currentPage, + pageCount: session.pages.length, + strokes: session.pages[currentPage].strokes, + })); + break; + } + + // ── Add page ─────────────────────────────────────────────── + case 'add_page': { + if (!session) return; + const idx = session.pages.length; + const name = `Page ${idx + 1}`; + session.pages.push({ name, strokes: [] }); + // Broadcast to all clients + broadcastToSession(currentSessionId, { + type: 'page_added', + page: idx, + name, + pageCount: session.pages.length, + }); + break; + } + + // ── Delete page ──────────────────────────────────────────── + case 'delete_page': { + if (!session) return; + const delPage = parseInt(msg.page, 10); + if (isNaN(delPage) || delPage < 0 || delPage >= session.pages.length) { + ws.send(JSON.stringify({ type: 'error', message: `Invalid page: ${delPage}` })); + return; + } + if (session.pages.length <= 1) { + ws.send(JSON.stringify({ type: 'error', message: 'Cannot delete the last page' })); + return; + } + session.pages.splice(delPage, 1); + const newCurrent = Math.min(currentPage, session.pages.length - 1); + currentPage = newCurrent; + broadcastToSession(currentSessionId, { + type: 'page_deleted', + page: delPage, + pageCount: session.pages.length, + currentPage: newCurrent, + strokes: session.pages[newCurrent].strokes, + }); + break; + } + + // ── Pages info ───────────────────────────────────────────── + case 'pages_info': { + if (!session) return; + ws.send(JSON.stringify({ + type: 'pages_info', + currentPage, + pages: session.pages.map((p, i) => ({ index: i, name: p.name, strokeCount: p.strokes.length })), + })); + break; + } + + // ── Batch strokes (offline sync) ────────────────────────── + case 'batch_strokes': { + if (!session) { + ws.send(JSON.stringify({ type: 'error', message: 'Not in a session' })); + return; + } + if (!msg.strokes || !Array.isArray(msg.strokes)) return; + const stored = []; + for (const stroke of msg.strokes) { + const pIdx = stroke.page !== undefined ? Math.min(stroke.page, session.pages.length - 1) : currentPage; + const page = session.pages[pIdx]; + if (!page) continue; + const full = { + points: stroke.points || [], + color: stroke.color || '#e94560', + size: stroke.size || 3, + isEraser: stroke.isEraser || false, + clientId, + id: generateId(), + page: pIdx, + }; + page.strokes.push(full); + if (page.strokes.length > MAX_STROKES_PER_PAGE) { + page.strokes.splice(0, page.strokes.length - MAX_STROKES_PER_PAGE); + } + stored.push(full); + } + if (stored.length > 0) { + broadcastToSession(currentSessionId, { type: 'batch_strokes', strokes: stored }, ws); + } + break; + } + + default: + ws.send(JSON.stringify({ type: 'error', message: `Unknown message type: ${msg.type}` })); + } + }); + + ws.on('close', () => { + if (currentSessionId) { + const s = sessions.get(currentSessionId); + if (s) { + s.clientIds.delete(clientId); + broadcastClientCount(currentSessionId); + } + } + // Clean up empty sessions + let changed = false; + for (const [sid, s] of sessions) { + if (s.clientIds.size === 0 && s.pages.every(p => p.strokes.length === 0)) { + sessions.delete(sid); + changed = true; + } + } + if (changed || currentSessionId) broadcastSessionList(); + }); +}); \ No newline at end of file diff --git a/git.hidrago.click.nginx b/git.hidrago.click.nginx new file mode 100644 index 0000000..b0a7f60 --- /dev/null +++ b/git.hidrago.click.nginx @@ -0,0 +1,14 @@ +server { + server_name git.hidrago.click; + + client_max_body_size 512M; + + location / { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + } +} \ No newline at end of file diff --git a/git.hidrago.click/docker-compose.yml b/git.hidrago.click/docker-compose.yml new file mode 100644 index 0000000..1ed8d0f --- /dev/null +++ b/git.hidrago.click/docker-compose.yml @@ -0,0 +1,62 @@ +# Gitea — self-hosted Git service +# https://git.hidrago.click +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=gitea + - GITEA__server__DOMAIN=git.hidrago.click + - GITEA__server__ROOT_URL=https://git.hidrago.click + - GITEA__server__HTTP_PORT=3000 + - GITEA__server__SSH_PORT=2222 + - GITEA__server__DISABLE_SSH=false + - GITEA__server__START_SSH_SERVER=true + - GITEA__server__SSH_DOMAIN=git.hidrago.click + - GITEA__server__LFS_START_SERVER=true + - GITEA__server__SSH_LISTEN_PORT=2222 + - GITEA__actions__ENABLED=true + - GITEA__admin__NAME=jerome + - GITEA__admin__PASSWD=test1234 + - GITEA__admin__EMAIL=hayakawa.guiard@gmail.com + restart: always + volumes: + - ./gitea-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:3001:3000" + - "2222:22" + depends_on: + db: + condition: service_healthy + networks: + - gitea + + db: + image: postgres:16-alpine + container_name: gitea-db + restart: always + environment: + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=gitea + - POSTGRES_DB=gitea + volumes: + - ./postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gitea"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - gitea + +networks: + gitea: + name: gitea \ No newline at end of file diff --git a/suma.hidrago.click.nginx b/suma.hidrago.click.nginx new file mode 100644 index 0000000..7a4f9e1 --- /dev/null +++ b/suma.hidrago.click.nginx @@ -0,0 +1,32 @@ +server { + server_name suma.hidrago.click; + + root /home/jerome/hermes-sites/suma.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + listen 443 ssl; # managed by Certbot + listen [::]:443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/suma.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/suma.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = suma.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + server_name suma.hidrago.click; + + listen 80; + listen [::]:80; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/suma.hidrago.click/generate_suma.py b/suma.hidrago.click/generate_suma.py new file mode 100644 index 0000000..13d642d --- /dev/null +++ b/suma.hidrago.click/generate_suma.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python3 +"""Scrape sumo results from sumodb.sumogames.de and generate the Suma website.""" + +import re +import json +import urllib.request +from datetime import datetime, timezone, timedelta +from html import unescape + +JST = timezone(timedelta(hours=9)) +PARIS = timezone(timedelta(hours=2)) # CEST (summer) + +BASHO_CODES = { + "01": ("Hatsu", "初場所", "Janvier"), + "03": ("Haru", "春場所", "Mars"), + "05": ("Natsu", "夏場所", "Mai"), + "07": ("Nagoya", "名古屋場所", "Juillet"), + "09": ("Aki", "秋場所", "Septembre"), + "11": ("Kyushu", "九州場所", "Novembre"), +} + +# ─── Scraping ─── + +def fetch_results(basho, day): + """Fetch and parse results for a given basho code (e.g. 202607) and day (1-15).""" + url = f"https://sumodb.sumogames.de/Results.aspx?b={basho}&d={day}" + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (SumaBot)"}) + with urllib.request.urlopen(req, timeout=30) as resp: + html = resp.read().decode("utf-8", errors="replace") + + # Header info + h1 = re.search(r"

([^<]+)

", html) + h2 = re.search(r"

([^<]+)

", html) + title = h1.group(1).strip() if h1 else f"Basho {basho} Day {day}" + date_str = h2.group(1).strip() if h2 else "" + + # Parse tables by division + tables = re.findall(r'', html, re.DOTALL) + divisions = {} + division_names = ["Makuuchi", "Juryo", "Makushita", "Sandanme", "Jonidan", "Jonokuchi"] + + for i, table in enumerate(tables): + if i >= len(division_names): + break + div_name = division_names[i] + matches = re.findall( + r'' + r'' + r'', + table, re.DOTALL) + parsed = [] + for m in matches: + east_result_img = m[0] + east_cell = m[1] + kim_cell = m[2] + west_cell = m[3] + west_result_img = m[4] + + # Parse wrestler info + east = parse_wrestler(east_cell) + west = parse_wrestler(west_cell) + technique = parse_technique(kim_cell) + + # Determine winner + east_won = "shiro" in east_result_img + west_won = "shiro" in west_result_img + east_lost = "kuro" in east_result_img + west_lost = "kuro" in west_result_img + fusen = "fusen" in east_result_img or "fusen" in west_result_img + + if east_won: + winner = "east" + elif west_won: + winner = "west" + elif fusen: + # Check who got the fusensho (win by default) + winner = "east" if "fusensho" in east_result_img else "west" + else: + winner = None + + parsed.append({ + "east": east, + "west": west, + "technique": technique, + "winner": winner, + "fusen": fusen, + }) + divisions[div_name] = parsed + + return { + "title": title, + "date_str": date_str, + "basho": basho, + "day": day, + "divisions": divisions, + } + + +def parse_wrestler(cell): + """Extract wrestler name, rank, record, and shikona from a cell.""" + rank = "" + rank_m = re.search(r'(.*?)', cell) + if rank_m: + rank = unescape(rank_m.group(1).strip()) + + # Japanese shikona from title + shikona_ja = "" + title_m = re.search(r"title='([^']+)'", cell) + if title_m: + parts = title_m.group(1).split(",") + if parts: + shikona_ja = parts[0].strip() + + # English shikona from link text + name = "" + link_m = re.search(r"href='Rikishi\.aspx\?r=\d+'>([^<]+)", cell) + if link_m: + name = link_m.group(1).strip() + + # Record + record = "" + rec_m = re.search(r'([^<]+)', cell) + if rec_m: + record = rec_m.group(1).strip() + + return {"rank": rank, "name": name, "shikona_ja": shikona_ja, "record": record} + + +def parse_technique(cell): + """Extract winning technique from kimari cell.""" + # Remove HTML tags + text = re.sub(r"<[^>]+>", "", cell) + text = unescape(text).strip() + # The technique is on its own line (after the first
) + lines = [l.strip() for l in text.split("\n") if l.strip()] + # Filter out H2H records (contain digits) + tech = "" + for line in lines: + if not re.search(r"\d", line) and line not in ("", " "): + tech = line + break + return tech + + +# ─── HTML generation ─── + +def generate_html(data): + now_utc = datetime.now(timezone.utc) + now_jst = now_utc.astimezone(JST) + now_paris = now_utc.astimezone(PARIS) + + basho_code = data["basho"] + month_code = basho_code[4:6] + year = basho_code[:4] + basho_en, basho_ja, basho_fr = BASHO_CODES.get(month_code, ("Unknown", "不明", "Inconnu")) + + # Build division sections + divisions_html = [] + for div_name in ["Makuuchi", "Juryo", "Makushita", "Sandanme", "Jonidan", "Jonokuchi"]: + matches = data["divisions"].get(div_name, []) + if not matches: + continue + matches_html = [] + for m in matches: + east = m["east"] + west = m["west"] + winner = m["winner"] + + # Winner/loser styling + east_class = "winner" if winner == "east" else ("loser" if winner == "west" else "") + west_class = "winner" if winner == "west" else ("loser" if winner == "east" else "") + + tech_display = m["technique"] if m["technique"] and not m["fusen"] else "" + if m["fusen"]: + tech_display = "不戦勝" if winner == "east" else "不戦勝" + + match_html = f"""
+
+ {east['rank']} + {east['name']} + {east['shikona_ja']} + {east['record']} +
+
{tech_display}
+
+ {west['rank']} + {west['name']} + {west['shikona_ja']} + {west['record']} +
+
""" + matches_html.append(match_html) + + matches_str = "\n".join(matches_html) + div_html = f"""
+

{div_name}

+{matches_str} +
""" + divisions_html.append(div_html) + + divisions_str = "\n".join(divisions_html) + + html = f""" + + + + + Suma — 相撲結果 + + + +
+

🤼 Suma

+
{year} {basho_fr} · {basho_ja}
+
Jour {data['day']}
+
{data['date_str']}
+
+
+ + Mise à jour automatique toutes les 10 minutes +
+
+{divisions_str} +
+
+ Données : sumodb.sumogames.de · + Dernière mise à jour : {now_paris.strftime('%d/%m/%Y %H:%M')} (Paris) / {now_jst.strftime('%H:%M')} (JST) +
+ +""" + return html + + +# ─── Auto-detect current basho and day ─── + +def detect_current_basho_day(): + """Determine the current basho code and day based on the date.""" + now_jst = datetime.now(JST) + year = now_jst.year + month = now_jst.month + + # Basho months: 1, 3, 5, 7, 9, 11 + basho_months = {1: "01", 3: "03", 5: "05", 7: "07", 9: "09", 11: "11"} + + # Find the most recent basho + basho_code = None + for m in [1, 3, 5, 7, 9, 11]: + if month >= m: + basho_code = f"{year}{basho_months[m]}" + elif month < 3: + basho_code = f"{year-1}11" + break + + # If no basho yet this year, use last November + if basho_code is None: + basho_code = f"{year-1}11" + + # Basho runs for 15 days starting on the 2nd Sunday of the month + # For simplicity, try days 1-15 and use the latest one with data + # First try day 15, then go down + for day in range(15, 0, -1): + try: + data = fetch_results(basho_code, day) + if data["divisions"]: + return basho_code, day, data + except Exception: + continue + + return basho_code, 1, None + + +if __name__ == "__main__": + import sys + + if len(sys.argv) >= 3: + basho = sys.argv[1] + day = int(sys.argv[2]) + else: + basho, day, pre_data = detect_current_basho_day() + if pre_data: + data = pre_data + else: + data = fetch_results(basho, day) + + if len(sys.argv) < 3: + # Auto mode + if 'data' not in dir(): + data = fetch_results(basho, day) + else: + data = fetch_results(basho, day) + + html = generate_html(data) + output_path = "/home/jerome/hermes-sites/suma.hidrago.click/index.html" + with open(output_path, "w", encoding="utf-8") as f: + f.write(html) + print(f"Suma page generated: basho={data['basho']} day={data['day']} -> {output_path}") diff --git a/suma.hidrago.click/index.html b/suma.hidrago.click/index.html new file mode 100644 index 0000000..5f628c4 --- /dev/null +++ b/suma.hidrago.click/index.html @@ -0,0 +1,1936 @@ + + + + + + Suma — 相撲結果 + + + +
+

🤼 Suma

+
2026 Juillet · 名古屋場所
+
Jour 15
+
July 26, 2026
+
+
+ + Mise à jour automatique toutes les 10 minutes +
+
+
+

Makuuchi

+
+
+ S1w + Kotoshoho + 琴勝峰 + 6-9 +
+
+
+ Y1w + Onosato + 大の里 + 9-6 +
+
+
+
+ O1e + Kirishima + 霧島 + 12-3 +
+
+
+ O1w + Kotozakura + 琴櫻 + 8-7 +
+
+
+
+ S1e + Atamifuji + 熱海富士 + 12-3 +
+
+
+ S2w + Aonishiki + 安青錦 + 12-3 +
+
+
+
+ K1e + Yoshinofuji + 義ノ富士 + 6-9 +
+
+
+ M4e + Daieisho + 大栄翔 + 10-5 +
+
+
+
+ M3w + Hakunofuji + 伯乃富士 + 9-6 +
+
+
+ K1w + Oho + 王鵬 + 2-13 +
+
+
+
+ M1e + Fujinokawa + 藤ノ川 + 8-7 +
+
+
+ M6w + Fujiseiun + 藤青雲 + 7-8 +
+
+
+
+ M2w + Churanoumi + 美ノ海 + 7-8 +
+
+
+ M1w + Takanosho + 隆の勝 + 6-9 +
+
+
+
+ M2e + Gonoyama + 豪ノ山 + 7-8 +
+
+
+ M3e + Hiradoumi + 平戸海 + 4-11 +
+
+
+
+ M9w + Tobizaru + 翔猿 + 5-10 +
+
+
+ M4w + Ichiyamamoto + 一山本 + 6-9 +
+
+
+
+ M5e + Ura + 宇良 + 5-10 +
+
+
+ M12w + Abi + 阿炎 + 7-8 +
+
+
+
+ M15e + Onokatsu + 阿武剋 + 4-11 +
+
+
+ M5w + Oshoma + 欧勝馬 + 7-8 +
+
+
+
+ M6e + Shodai + 正代 + 5-10 +
+
+
+ M16e + Daiseizan + 大青山 + 6-9 +
+
+
+
+ M7e + Kotoeiho + 琴栄峰 + 11-4 +
+
+
+ M13w + Takerufuji + 尊富士 + 10-5 +
+
+
+
+ M10e + Asanoyama + 朝乃山 + 9-6 +
+
+
+ M7w + Takayasu + 高安 + 11-4 +
+
+
+
+ M8e + Wakamotoharu + 若元春 + 6-9 +
+
+
+ M12e + Asahakuryu + 朝白龍 + 7-8 +
+
+
+
+ M14w + Shishi + 獅司 + 10-5 +
+
+
+ M8w + Roga + 狼雅 + 9-6 +
+
+
+
+ M9e + Fujiryoga + 藤凌駕 + 10-5 +
+
+
+ M16w + Asakoryu + 朝紅龍 + 9-6 +
+
+
+
+ M13e + Nishikifuji + 錦富士 + 10-5 +
+
+
+ M10w + Chiyoshoma + 千代翔馬 + 5-10 +
+
+
+
+ M14e + Kinbozan + 金峰山 + 9-6 +
+
+
+ M11w + Mitakeumi + 御嶽海 + 2-13 +
+
+
+
+ J2w + Sadanoumi + 佐田の海 + 5-10 +
+
+
+ M15w + Kazuma + 一意 + 5-10 +
+
+
+
+

Juryo

+
+
+ J1e + Asasuiryu + 朝翠龍 + 11-4 +
+
+
+ J8w + Hatsuyama + 羽出山 + 7-8 +
+
+
+
+ J13e + Arashifuji + 嵐富士 + 11-4 +
+
+
+ J1w + Kyokukaiyu + 旭海雄 + 7-8 +
+
+
+
+ J2e + Tokihayate + 時疾風 + 10-5 +
+
+
+ J6e + Shonannoumi + 湘南乃海 + 11-4 +
+
+
+
+ J3e + Ryuden + 竜電 + 4-11 +
+
+
+ J9e + Shirokuma + 白熊 + 9-6 +
+
+
+
+ J7e + Tamawashi + 玉鷲 + 9-6 +
+
+
+ J3w + Dewanoryu + 出羽ノ龍 + 9-6 +
+
+
+
+ J4e + Tomokaze + 友風 + 5-10 +
+
+
+ J10e + Hitoshi + 日翔志 + 6-9 +
+
+
+
+ J11e + Kagayaki + 輝 + 7-8 +
+
+
+ J4w + Toshinofuji + 寿之富士 + 11-4 +
+
+
+
+ J5e + Kitanowaka + 北の若 + 7-8 +
+
+
+ J14e + Tochitaikai + 栃大海 + 7-8 +
+
+
+
+ J9w + Nishinoryu + 西ノ龍 + 4-11 +
+
+
+ J6w + Kazekeno + 風賢央 + 8-7 +
+
+
+
+ J12w + Kayo + 嘉陽 + 7-8 +
+
+
+ J7w + Meisei + 明生 + 4-11 +
+
+
+
+ J8e + Midorifuji + 翠富士 + 8-7 +
+
+
+ J14w + Nishikigi + 錦木 + 7-8 +
+
+
+
+ Ms2e + Gojinyu + 豪刃雄 + 3-5 +
+
+
+ J10w + Tamashoho + 玉正鳳 + 6-9 +
+
+
+
+ Ms4e + Mudoho + 夢道鵬 + 3-4 +
+
+
+ J11w + Enho + 炎鵬 + 8-7 +
+
+
+
+ J12e + Tohakuryu + 東白龍 + 8-7 +
+
+
+ Ms5e + Tochikodai + 栃幸大 + 4-3 +
+
+
+
+

Makushita

+
+
+ Ms10w + Kaiseijo + 魁清城 + 2-5 +
+
+
+ Ms6w + Sazanami + 漣 + 3-4 +
+
+
+
+ Ms7e + Ikarigata + 碇潟 + 4-3 +
+
+
+ Ms11w + Fujisodai + 藤壮大 + 5-2 +
+
+
+
+ Ms9e + Gonoumi + 豪ノ湖 + 2-5 +
+
+
+ Ms15e + Asonoyama + 阿蘇ノ山 + 1-6 +
+
+
+
+ Ms13e + Hokutenkai + 北天海 + 4-3 +
+
+
+ Ms15w + Daiyusho + 大雄翔 + 5-2 +
+
+
+
+ Ms18e + Toshunryu + 東俊隆 + 5-2 +
+
+
+ Ms14w + Kyokumizuno + 旭水野 + 4-3 +
+
+
+
+ Ms25w + Gyotoku + 行徳 + 5-2 +
+
+
+ Ms24w + Tokunomusashi + 徳之武藏 + 4-3 +
+
+
+
+ Ms27e + Ikazuchido + 雷道 + 1-6 +
+
+
+ Ms34w + Ienoshima + 家の島 + 2-5 +
+
+
+
+ Ms31w + Koseiryu + 光星竜 + 5-2 +
+
+
+ Ms27w + Nihonyanagi + 二本柳 + 4-3 +
+
+
+
+ Ms32w + Shimanoumi + 志摩ノ海 + 5-2 +
+
+
+ Ms29w + Obara + 小原 + 4-3 +
+
+
+
+ Ms33e + Shimizuumi + 清水海 + 2-5 +
+
+
+ Ms35w + Tokitenran + 時天嵐 + 3-4 +
+
+
+
+ Ms38e + Nionoumi + 鳰の湖 + 2-5 +
+
+
+ Ms43e + Kiyonoumi + 清乃海 + 3-4 +
+
+
+
+ Ms44e + Kaisho + 魁勝 + 5-2 +
+
+
+ Ms40w + Hananofuji + 花の富士 + 4-3 +
+
+
+
+ Ms45e + Kakueizan + 鶴英山 + 5-2 +
+
+
+ Ms47e + Haruyama + 春山 + 4-3 +
+
+
+
+ Ms52e + Okinohama + 隠岐の浜 + 3-4 +
+
+
+ Ms45w + Gonosho + 豪ノ勝 + 2-5 +
+
+
+
+ Ms48e + Nishiomoto + 西御許 + 4-3 +
+
+
+ Ms51w + Anosho + 安大翔 + 5-2 +
+
+
+
+ Ms53e + Yago + 矢後 + 1-6 +
+
+
+ Sd5w + Furuta + 古田 + 2-5 +
+
+
+
+ Ms54e + Yonezawaryu + 米沢龍 + 5-2 +
+
+
+ Ms58e + Kaida + 甲斐田 + 4-3 +
+
+
+
+ Ms60e + Oginohama + 小城ノ浜 + 3-4 +
+
+
+ Ms60w + Toshoyama + 東照山 + 2-5 +
+
+
+
+

Sandanme

+
+
+ Sd5e + Kototaiko + 琴泰広 + 4-3 +
+
+
+ Sd9e + Kitaharima + 北磻磨 + 5-2 +
+
+
+
+ Sd6e + Mienofuji + 三重ノ富士 + 6-1 +
+
+
+ Sd24w + Kototsubasa + 琴翼 + 5-2 +
+
+
+
+ Sd11w + Hamayutaka + 濱豊 + 5-2 +
+
+
+ Sd7w + Tochinobori + 栃登 + 4-3 +
+
+
+
+ Sd10e + Shunrai + 春雷 + 2-5 +
+
+
+ Sd17w + Tokikenryu + 時健龍 + 3-4 +
+
+
+
+ Sd15e + Okada + 岡田 + 5-2 +
+
+
+ Sd21e + Izumifuji + 泉富士 + 4-3 +
+
+
+
+ Sd17e + Chiyonokatsu + 千代の勝 + 2-5 +
+
+
+ Sd26w + Inaba + 稲葉 + 1-6 +
+
+
+
+ Sd23w + Kuranofuji + 蔵ノ富士 + 3-4 +
+
+
+ Sd19w + Gohakuun + 豪白雲 + 2-5 +
+
+
+
+ Sd22e + Yamato + 山藤 + 4-3 +
+
+
+ Sd26e + Shosei + 翔盛 + 5-2 +
+
+
+
+ Sd25e + Minorufuji + 実富士 + 2-5 +
+
+
+ Sd28w + Ebisumaru + 恵比寿丸 + 3-4 +
+
+
+
+ Sd42e + Karino + 狩野 + 2-5 +
+
+
+ Sd31w + Kototaiga + 琴大河 + 1-6 +
+
+
+
+ Sd33e + Wakenokaze + 和気乃風 + 3-4 +
+
+
+ Sd35e + Chiyotaiyo + 千代太陽 + 2-5 +
+
+
+
+ Sd40e + Uchima + 内間 + 3-4 +
+
+
+ Sd41e + Amakaze + 天風 + 2-5 +
+
+
+
+ Sd46w + Yutakasho + 豊翔 + 3-4 +
+
+
+ Sd40w + Hakuomaru + 柏王丸 + 2-5 +
+
+
+
+ Sd53e + Shuji + 周志 + 1-3-3 +
+
+
+ Sd43w + Kikunoshiro + 菊ノ城 + 2-5 +
+
+
+
+ Sd49e + Shoji + 庄司 + 5-2 +
+
+
+ Sd45w + Yoshiyasu + 良安 + 4-3 +
+
+
+
+ Sd59w + Kaishinmaru + 魁新丸 + 5-2 +
+
+
+ Sd52w + Kanpuzan + 寒風山 + 6-1 +
+
+
+
+ Sd67w + Tochimitsuru + 栃満 + 1-6 +
+
+
+ Sd60w + Ryuseiyama + 龍成山 + 2-5 +
+
+
+
+ Sd68e + Kotakiyama + 小滝山 + 5-2 +
+
+
+ Sd63w + Churafuji + 美富士 + 4-3 +
+
+
+
+ Sd65e + Hikarumusashi + 光武蔵 + 2-5 +
+
+
+ Sd67e + Kanryu + 寛龍 + 3-4 +
+
+
+
+ Jd1e + Daitenshin + 大天真 + 2-5 +
+
+
+ Sd69w + Fujiyuho + 藤雄峰 + 3-4 +
+
+
+
+ Sd80TD + Sawada + 澤田 + 5-2 +
+
+
+ Sd70w + Miyanokaze + 宮乃風 + 6-1 +
+
+
+
+ Sd72e + Ikazuchishu + 雷修 + 3-4 +
+
+
+ Sd73e + Hitachigo + 常陸號 + 2-5 +
+
+
+
+ Sd75e + Kiyonoyama + 清の山 + 5-2 +
+
+
+ Sd79w + Tsugunohana + 禎ノ花 + 4-3 +
+
+
+
+ Jd1w + Kitanosho + 北乃庄 + 5-2 +
+
+
+ Sd76w + Aron + 阿龍 + 6-1 +
+
+
+
+ Sd78e + Sakabayashi + 坂林 + 2-5 +
+
+
+ Jd16w + Wakahomura + 若焔 + 1-6 +
+
+
+
+

Jonidan

+
+
+ Jd2e + Oginosho + 小城ノ正 + 3-4 +
+
+
+ Jd6w + Ryubumaru + 流武丸 + 2-5 +
+
+
+
+ Jd11e + Ojiyama + 応時山 + 3-4 +
+
+
+ Jd3w + Fukuazuma + 福東 + 2-5 +
+
+
+
+ Jd7w + Dewanojo + 出羽ノ城 + 4-3 +
+
+
+ Jd4w + Shunta + 駿太 + 5-2 +
+
+
+
+ Jd15w + Takaarashi + 隆嵐 + 5-2 +
+
+
+ Jd8w + Ikazuchiarashi + 雷嵐 + 4-3 +
+
+
+
+ Jd9e + Kaihiryu + 魁陽龍 + 5-2 +
+
+
+ Jd10e + Keiga + 恵雅 + 4-3 +
+
+
+
+ Jd14e + Tanaka + 田中 + 2-5 +
+
+
+ Jd20e + Daitengu + 大天狗 + 3-4 +
+
+
+
+ Jd23e + Satsumasho + 薩摩翔 + 5-2 +
+
+
+ Jd20w + Shimanishiki + 志摩錦 + 4-3 +
+
+
+
+ Jd22e + Mihamaumi + 美浜海 + 5-2 +
+
+
+ Jd28e + Tsukioka + 月岡 + 6-1 +
+
+
+
+ Jd26e + Kikuchi + 菊池 + 2-5 +
+
+
+ Jd34e + Matsuzawa + 松澤 + 3-4 +
+
+
+
+ Jd27e + Masakifuji + 柾富士 + 5-2 +
+
+
+ Jd35w + Tsurugifuji + 都留樹富士 + 4-3 +
+
+
+
+ Jd34w + Tochigidake + 栃岐岳 + 2-5 +
+
+
+ Jd28w + Watatani + 綿谷 + 1-6 +
+
+
+
+ Jd33e + Kawazoe + 川副 + 6-1 +
+
+
+ Jd42e + Ryutsukasa + 龍司 + 5-2 +
+
+
+
+ Jd39e + Matsugashima + 松ヶ島 + 2-5 +
+
+
+ Jd37w + Takasu + 高須 + 3-4 +
+
+
+
+ Jd44e + Kikushoki + 菊昇基 + 4-3 +
+
+
+ Jd47w + Buomaru + 武旺丸 + 5-2 +
+
+
+
+ Jd45e + Azuma + 安寿真 + 3-4 +
+
+
+ Jd49w + Sonoshun + 蒼乃駿 + 2-5 +
+
+
+
+ Jd48e + Tosashimizu + 土佐清水 + 5-2 +
+
+
+ Jd50w + Kakusho + 鶴翔 + 4-3 +
+
+
+
+ Jd52e + Motoazuma + 本東 + 2-5 +
+
+
+ Jd48w + Hishuyama + 肥州山 + 1-6 +
+
+
+
+ Jd53w + Akinishiki + 安芸錦 + 5-2 +
+
+
+ Jd51w + Reonmaru + 黎大丸 + 4-3 +
+
+
+
+ Jd54e + Masarufuji + 優富士 + 5-2 +
+
+
+ Jd59w + Kukio + 九鬼王 + 4-3 +
+
+
+
+ Jd55e + Biganzan + 備巌山 + 2-5 +
+
+
+ Jd57e + Shibuya + 澁谷 + 3-4 +
+
+
+
+ Jd60e + Dewanosora + 出羽の空 + 4-3 +
+
+
+ Jd66e + Soedayama + 添田山 + 5-2 +
+
+
+
+ Jd61e + Hamadayama + 浜田山 + 5-2 +
+
+
+ Jd68w + Umeyama + 梅山 + 4-3 +
+
+
+
+ Jd65e + Boshuyama + 房州山 + 5-2 +
+
+
+ Jd67e + Ryusei + 隆晟 + 6-1 +
+
+
+
+ Jd78e + Urutora + 宇瑠寅 + 2-5 +
+
+
+ Jd74w + Sumiyoshi + 住吉 + 3-4 +
+
+
+
+ Jd77e + Tokio + 鬨王 + 2-5 +
+
+
+ Jd82e + Yamauchi + 山内 + 3-4 +
+
+
+
+ Jd80e + Setonoryu + 瀬戸ノ竜 + 1-6 +
+
+
+ Jd81w + Kotodaigo + 琴太豪 + 2-0-5 +
+
+
+
+ Jd87e + Chiyokozan + 千代煌山 + 3-4 +
+
+
+ Jd90w + Sadanojo + 佐田の城 + 2-3-2 +
+
+
+
+ Jd96e + Sanukiryu + 讃岐竜 + 4-3 +
+
+
+ Jd89w + Daibasho + 大馬翔 + 5-2 +
+
+
+
+ Jk1w + Aratobi + 荒飛 + 4-3 +
+
+
+ Jd96w + Takatairiku + 隆太陸 + 5-2 +
+
+
+
+ Jk5e + Fukushoryu + 福生龍 + 5-2 +
+
+
+ Jd99w + Tomimaru + 冨丸 + 4-3 +
+
+
+
+

Jonokuchi

+
+
+ Jk2e + Wakaarata + 若花新 + 4-3 +
+
+
+ Jk7e + Kotohiranuma + 琴平沼 + 3-4 +
+
+
+
+ Jk3w + Chiyooume + 千代青梅 + 3-4 +
+
+
+ Jk2w + Akenoyama + 安氣乃山 + 2-5 +
+
+
+
+ Jk6e + Okuniyama + 大國山 + 2-5 +
+
+
+ Jk6w + Azumayama + 東山 + 3-4 +
+
+
+
+ Jk8e + Usagifuji + 兎富士 + 3-4 +
+
+
+ Jk18e + Ogawa + 小川 + 5-2 +
+
+
+
+ Jk20e + Honda + 本多 + 1-6 +
+
+
+ Jk9w + Kotoishida + 琴石田 + 2-5 +
+
+
+
+ Jk10e + Shimano + 島野 + 2-5 +
+
+
+ Jk12e + Kazenohana + 風ノ華 + 3-4 +
+
+
+
+ Jk13w + Kyonosato + 京の里 + 2-5 +
+
+
+ Jk10w + Chiyoshinzan + 千代真山 + 3-4 +
+
+
+
+ Jk17e + Uza + 宇座 + 3-4 +
+
+
+ Jk19w + Dewanowaka + 出羽乃若 + 4-3 +
+
+
+
+
+ Données : sumodb.sumogames.de · + Dernière mise à jour : 26/07/2026 11:05 (Paris) / 18:05 (JST) +
+ + \ No newline at end of file diff --git a/summer-2026.hidrago.click.nginx b/summer-2026.hidrago.click.nginx new file mode 100644 index 0000000..20fde39 --- /dev/null +++ b/summer-2026.hidrago.click.nginx @@ -0,0 +1,32 @@ +server { + server_name summer-2026.hidrago.click; + + root /home/jerome/hermes-sites/summer-2026.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/summer-2026.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/summer-2026.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = summer-2026.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + server_name summer-2026.hidrago.click; + + listen 80; + listen [::]:80; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/summer-2026.hidrago.click/index.html b/summer-2026.hidrago.click/index.html new file mode 100644 index 0000000..59837a2 --- /dev/null +++ b/summer-2026.hidrago.click/index.html @@ -0,0 +1,955 @@ + + + + + + Summer 2026 — Éclipse & Campings + + + + +
+

🌞 Éclipse Solaire du 12 Août 2026

+

Campings & clubs familiaux avec toboggans, activités enfants et chiens admis — du Pays Basque aux Asturies, et dans toute l'Espagne du Nord pour la totalité

+
+ + +
+
⏱ Mercredi 12 août 2026 — 20h27 ~ 20h32 (heure locale espagnole)
+
+ Totalité: l'éclipse traverse le nord de l'Espagne d'ouest en est, de la Galice aux Baléares.
+ Visible depuis A Coruña · Gijón · Oviedo · Santander · Bilbao · Vitoria · Burgos · Valladolid · Palencia · Zaragoza · Valence · Palma (Majorque) et bien d'autres villes.
+ Madrid et Barcelone sont juste en dehors de la bande de totalité — mais y verront une éclipse partielle à ~90%. +
+
🌑 La dernière éclipse totale visible en Espagne continentale datait de 1905 — et la prochaine arrive dès 2027 !
+
+ + +
+
+
2m 18s
+
Durée max mondiale (Islande)
+
+
+
1m 49s
+
Durée max Espagne (Oviedo)
+
+
+
1.0386
+
Magnitude maximale
+
+
+
2027
+
Prochaine totale en Espagne
+
+
+ + +
+

⏱️ Durée de totalité par ville

+

+ Classement par durée de totalité — pour choisir votre spot idéal +

+
(.*?)(.*?)(.*?)(.*?)(.*?)
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#VilleDébut totalitéFin totalitéDuréeFin éclipse
🥇Oviedo ⭐20:27:0520:28:541m 49s21:21
🥇Gijón ⭐20:26:4820:28:331m 45s21:20
🥇Burgos ⭐20:28:2420:30:081m 44s21:20 (couché)
4Palencia20:29:0820:30:511m 43s21:22
5Palma (Majorque)20:31:0520:32:411m 36s20:49 (couché)
6Teruel20:31:0420:32:381m 34s21:05
7Alcúdia (Majorque)20:30:3420:32:031m 29s20:48 (couché)
8Valladolid20:29:5320:31:201m 27s21:22
9Ponferrada20:28:4420:30:111m 27s21:22
10Zaragoza20:29:0220:30:271m 25s21:07 (couché)
11Logroño20:28:0920:29:311m 22s21:15 (couché)
12A Coruña20:27:4020:28:571m 17s21:22
13Santander20:26:5720:28:011m 04s21:20
14Vitoria-Gasteiz20:27:4320:28:471m 04s21:17 (couché)
+

+ ⏰ Heures locales espagnoles (UTC+2) · Source: Wikipedia · Données NASA +

+ + + +
+

🗺️ Carte du trajet de la totalité

+ Carte de l'éclipse du 12 août 2026 +
+ A Coruña 20:28 1m17 + Gijón 20:27 1m45 + Oviedo 20:28 1m49 + Santander 20:27 1m04 + Bilbao 20:27 0m31 + Vitoria 20:28 1m04 + Burgos 20:29 1m44 + Logroño 20:28 1m22 + Valladolid 20:30 1m27 + Zaragoza 20:30 1m25 + Valence 20:30 + Palma 20:31 1m36 +
+
+ + +
+

🏕️ Nos propositions de campings

+

Tous acceptent les chiens, ont des toboggans aquatiques et des activités encadrées pour Louis (CE1) et Maël (GS)

+ + +
+

🥇 Asturies & Cantabrie — Totalité maximale

+

Jusqu'à 1m49s de totalité ! La côte cantabrique offre les plus longues durées d'Espagne.

+
+ + +
+
+

🌊 Camping Deva ⭐⭐⭐

+ Asturies +
+
📍 Deva, Gijón (Asturies, Espagne) — DANS la zone de totalité
+
+ 🏊 Piscine + 🎨 Aire de jeux enfants + 🐕 Chiens admis + 🌊 Plage à 300m + 🌑 Totalité 1m45s + 🏖️ 5 plages à proximité +
+
+ ⭐ LE MEILLEUR SPOT POUR LA TOTALITÉ ! Camping familial à 5 min de Gijón et 15 min d'Oviedo. Sur la côte cantabrique, avec le plus longue durée de totalité de toutes nos options (1m45s). Plage de Deva à 300m, proche du centre de Gijón. Note 8,2/10 — excellent rapport qualité-prix pour être au cœur de l'événement. +
+
+ 🌑 Totalité sur place ! Gijón à 5min (1m45s) · Oviedo à 15min (1m49s) +
+
À partir de ~40€/nuit en mobil-home
+
+ + +
+
+

🇪🇸 Camping Playa de Vega ⭐⭐⭐

+ Espagne - Asturies +
+
📍 Ribadesella, Asturies (Espagne) — DANS la zone de totalité, côte cantabrique
+
+ 🏊 Piscine & toboggans + 🎨 Aire de jeux & mini-club + 🐕 Chiens admis + 🏖️ Plage à 200m + 🧗 Grottes de Tito Bustillo (UNESCO) + 🌑 Totalité 1m45s+ +
+
+ ⭐ DANS LA ZONE DE TOTALITÉ! Camping familial sur la côte des Asturies, à proximité de Ribadesella. Plage de Vega à 200m. Piscine avec toboggans, aire de jeux, mini-club en saison. Les grottes préhistoriques de Tito Bustillo (UNESCO) sont à 5 min. Note 4,3/5 sur Google (1200+ avis). Durée de totalité estimée ~1m45 (entre Oviedo et Gijón). +
+
+ 🌑 Totalité sur place! — ~20:27, durée ~1min 45s +
+
À partir de ~50€/nuit en mobil-home
+ Voir & réserver → +
+ + +
+
+

🇪🇸 Camping Playa Liencres ⭐⭐⭐

+ Espagne - Cantabrie +
+
📍 Liencres, Cantabrie (Espagne) — à 15km à l'ouest de Santander, DANS la bande de totalité
+
+ 🏊 Piscine & accès plage + 🎨 Aire de jeux enfants + 🐕 Chiens admis + 🏖️ Plage à 800m + 🌲 Parc naturel de Liencres + 🌑 Totalité 1m04s +
+
+ ⭐ DANS LA ZONE DE TOTALITÉ! Camping situé à 15km de Santander, sur la côte cantabre. Vous pourrez voir l'éclipse totale sans aucune route à faire! Parc naturel de Liencres, plages sauvages et dunaires. Idéal pour combiner vacances en famille et éclipse. Durée: 1m04s. +
+
+ 🌑 Totalité sur place! — 20:27, durée ~1m04s +
+
Tarifs à confirmer sur le site
+ Voir & réserver → +
+ + +
+

🥈 Pays Basque & Landes — Option équilibrée

+

Moins de route depuis la France, trajet de 1h30-2h vers Bilbao ou Vitoria le jour J.

+
+ + +
+
+

🏖️ Camping Sud Land ⭐⭐⭐⭐

+ Capfun +
+
📍 Labenne-Océan (40), Landes — à 1,4km des plages et 15km de Bayonne
+
+ 🏊 Toboggans Crazy Cone & Double Tube + Piscine couverte & extérieure chauffées + 🎨 Club Clapou (4-6 ans) + Club Politicator (7-10 ans), 4j/semaine + 🐕 Chiens admis (35€/semaine BS, 42€ HS) + 🚴 Location de vélos + Commerces à pied +
+
+ L'idéal pour découvrir à la fois les Landes et le Pays Basque. Plage et commerces à proximité, pistes cyclables de la Vélodyssée au pied du camping. Ambiance familiale, soirées mousse, château gonflable. Note 9,1/10 sur 1430 avis — 94% de clients veulent repartir. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h · Vitoria (1m04) à ~2h30 +
+
À partir de 350€ / semaine (Habana Top Presta 4pers)
+ Voir & réserver → +
+ + +
+
+

🎪 Camping Clown Océan ⭐⭐⭐⭐

+ Capfun +
+
📍 Tosse (40), Landes — à 10km des plages landaises
+
+ 🏊 Toboggans géants & Spacebowl + Piscine couverte avec toboggans + 🎨 Club Clapou (4-6 ans) + Club Politicator (7-10 ans) + 🐕 Chiens admis (35€/semaine basse saison, 42€ haute saison) + 🔥 Fun Dose Rouge (animations max) +
+
+ Camping original sur 4 hectares de couleurs, à 10km des plages. Animations maximum (Fun Dose Rouge) avec soirées mousse, spectacles et mascottes. Clubs enfants encadrés 4 jours/semaine. Note 9,2/10 — excellent rapport qualité-prix! +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~1h45 · Vitoria (1m04) à ~2h +
+
À partir de 315€ / semaine (Habana Top Presta 4pers) — le moins cher!
+ Voir & réserver → +
+ + +
+
+

🌅 Camping La Mer ⭐⭐⭐⭐

+ Capfun +
+
📍 Labenne (40), Landes — entre Biarritz et Hossegor, sur le littoral
+
+ 🏊 Spacebowl & toboggans à sensation + Piscine couverte avec toboggans + 🎨 Club Clapou (4-6 ans) + Club Politicator (7-10 ans), 4j/semaine + 🐕 Chiens admis (35€/semaine BS, 42€ HS) + 🏖️ 700m de la plage +
+
+ Situé à Labenne, entre Biarritz et Hossegor, à seulement 700m de la plage. Le Capfun le plus proche du Pays Basque et de l'Espagne. Spacebowl (toboggan à sensation), clubs enfants encadrés 4 jours/semaine. Note 9,1/10. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h · Vitoria (1m04) à ~2h +
+
À partir de 399€ / semaine (Habana Top Presta 4pers)
+ Voir & réserver → +
+ + +
+
+

🌲 Camping Lou Puntaou ⭐⭐⭐⭐⭐

+ Capfun +
+
📍 Léon (40), Landes — bordure du lac de Léon, réserve naturelle du Huchet
+
+ 🏊 Toboggans aquatiques & parc aquatique + Piscine chauffée + 🎨 Club enfants & animations + 🐕 Chiens admis (laisse, vaccinés) + 🛶 Lac & pédalos + 🌲 Réserve naturelle +
+
+ Ambiance calme et familiale en bordure du magnifique lac de Léon et de ses plages aménagées. Contact direct avec la nature dans la réserve du Huchet. Note 9,2/10 — l'un des mieux notés de Capfun! +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h · Vitoria (1m04) à ~2h30 +
+
À partir de 343€ / semaine (Habana Top Presta 4pers)
+ Voir & réserver → +
+ + +
+
+

🌊 Camping La Paillotte ⭐⭐⭐⭐

+ Capfun +
+
📍 Azur (40), Landes — bordure du lac de Soustons, à 7km de l'océan
+
+ 🏊 Toboggans aquatiques & parc aquatique + Piscine chauffée + 🎨 Club enfants & animations + 🚫 Chiens NON admis + 🚣 Lac pour activités nautiques +
+
+ Au cœur des Landes, bordé par le lac de Soustons. Concept unique avec cadre naturel et détente. Idéal pour les familles cherchant un cadre paisible avec un grand parc aquatique. Note 8,9/10. ⚠️ Attention: chiens non acceptés. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h15 · Santander (1m04) à ~3h15 +
+
À partir de 350€ / semaine (Habana Top Presta 4pers)
+ Voir & réserver → +
+ + +
+
+

⛰️ Camping Col d'Ibardin ⭐⭐⭐⭐

+ Homair +
+
📍 Urrugne (64), Pyrénées-Atlantiques — Pays Basque, au pied du col d'Ibardin, frontière espagnole
+
+ 🏊 Piscine + 🎨 Club enfants en saison + 🐕 Chiens admis + 🌊 5km des plages + 🏔️ Vue Pyrénées + 🚗 1h30 de Bilbao +
+
+ Le seul camping Homair directement au Pays Basque! Au col d'Ibardin, entre Saint-Jean-de-Luz et l'Espagne. Le camping français le plus proche de la zone de totalité — Bilbao à 1h30. Note 4,4/5 — le mieux noté des Pyrénées-Atlantiques sur Homair. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~1h30 · Vitoria (1m04) à ~1h45 +
+
Tarifs sur Homair
+ Voir sur Homair → +
+ + +
+
+

🇪🇸 Camping Landa Hiria ⭐⭐⭐

+ Espagne - Pays Basque +
+
📍 Hondarribia, Guipúzcoa (Espagne) — à la frontière franco-espagnole, proche de Hendaye
+
+ 🏊 Piscine & toboggans + 🎨 Mini-club & animations + 🐕 Chiens admis + 🏖️ Plage à 1km + 🏛️ Hondarribia (village médiéval) +
+
+ Camping familial au Pays Basque espagnol, à 5min de la frontière française. Situation idéale: vous êtes à ~1h45 de Bilbao et de sa zone de totalité. Piscine avec toboggans, mini-club en saison, animations. Proche du charmant village médiéval d'Hondarribia. Note 4,2/5 sur Google. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~1h45 · Vitoria (1m04) à ~1h30 +
+
À partir de ~45€/nuit
+ Voir & réserver → +
+ + +
+

🥉 Intérieur castillan (Burgos, Valladolid) — Meilleure météo

+

La côte cantabrique peut être nuageuse en août. L'intérieur (Burgos, Valladolid, Palencia) offre plus de chances de ciel dégagé.

+
+ + +
+
+

🌳 Camping El Tilo ⭐⭐

+ Indépendant +
+
📍 Burgos (Castille-et-León, Espagne) — DANS la zone de totalité
+
+ 🏊 Piscine extérieure + 🎨 Aire de jeux + 🐕 Chiens admis + 🌲 Cadre naturel + 🌑 Totalité 1m44s + ☀️ Meilleure météo +
+
+ ⭐ DANS LA ZONE DE TOTALITÉ — 1m44s! Camping simple et familial à Burgos. L'intérieur des terres (Burgos, Palencia, Valladolid) a statistiquement moins de nuages que la côte cantabrique en août. Piscine, aire de jeux, calme. Note 7,8/10. +
+
+ 🌑 Totalité sur place! — 20:29, durée 1m44s — ciel plus dégagé qu'en bord de mer +
+
À partir de ~30€/nuit
+ Voir & réserver → +
+ + +
+

🏝️ Baléares (Majorque) — Soleil garanti

+

La Méditerranée offre le meilleur climat pour l'éclipse. Palma et Alcúdia sont dans la bande de totalité !

+
+ + +
+
+

🇪🇸 Camping Son Moll ⭐⭐⭐

+ Majorque - Cala Ratjada +
+
📍 Cala Ratjada, Majorque (Baléares, Espagne) — DANS la bande de totalité
+
+ 🏊 Piscine & toboggans + 🎨 Mini-club & animations + 🐕 Chiens admis + 🏖️ Plage à 10min à pied + ☀️ Climat méditerranéen + 🌑 Totalité 1m30s+ +
+
+ ⭐ DANS LA ZONE DE TOTALITÉ! Camping familial sur la côte est de Majorque. Le meilleur climat d'Espagne pour l'éclipse — la Méditerranée a statistiquement le plus de jours dégagés en août. Piscine avec toboggans, mini-club, animations. Note 4/5. Durée de totalité estimée ~1m30. +
+
+ 🌑 Totalité sur place! — 20:31, durée ~1m30 — mais le Soleil se couche peu après (20:49) +
+
À partir de ~35€/nuit
+ Voir & réserver → +
+ + +
+

🔄 Alternatives supplémentaires

+

Si les options ci-dessus sont complètes, voici des plans B et C solides

+
+ + +
+
+

🐦 Camping Club Les Tourterelles ⭐⭐⭐⭐

+ MS Vacances +
+
📍 Saint-Girons Plage (40), Landes — bord de plage Atlantique
+
+ 🏊 Toboggans aquatiques + Piscine + 🎨 Club enfants & animations + 🐕 Chiens admis + 🏖️ Accès direct à la plage +
+
+ Camping Club 4* MS Vacances avec accès direct à la plage de Saint-Girons. Le style "club" que vous cherchez: animations, soirées, club enfants, restauration. Idéalement situé dans le sud des Landes, proche du Pays Basque et de la frontière espagnole. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h30 · Vitoria (1m04) à ~2h +
+
Tarifs à confirmer sur le site MS Vacances
+ Voir & réserver → +
+ + +
+
+

🌴 Camping Mayotte Vacances ⭐⭐⭐⭐⭐

+ Homair +
+
📍 Biscarrosse (40), Landes — proche du lac et de l'océan
+
+ 🏊 Parc aquatique avec toboggans + Piscine couverte & extérieure + 🎨 Club enfants & animations + 🐕 Chiens admis + 🏖️ Plage du lac +
+
+ Camping 5★ à Biscarrosse, l'un des mieux notés Homair dans les Landes (4,2/5). Grand parc aquatique, animations, club enfants. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h45 · Vitoria (1m04) à ~3h +
+
Tarifs sur Homair
+ Voir sur Homair → +
+ + +
+
+

🌊 Camping Lou Pignada ⭐⭐⭐⭐⭐

+ Homair +
+
📍 Sud des Landes / Pays Basque — frontière 64/40
+
+ 🏊 Parc aquatique avec toboggans + Piscine + 🎨 Club enfants + 🐕 Chiens admis +
+
+ Camping 5★ dans le sud des Landes / Pays Basque (4,1/5). Parc aquatique avec toboggans, club enfants. Bien situé pour l'Espagne. +
+
+ 🌑 Trajet totalité: Bilbao (31s) à ~2h · Vitoria (1m04) à ~2h +
+
Tarifs sur Homair
+ Voir sur Homair → +
+ +
+ + +
+

💡 Analyse & stratégie

+
    +
  • 🥇 Option A — Tout sur place (Asturies): Séjourner directement à Gijón ou Oviedo dans un camping local. Totalité maximale (1m45-1m49s), pas de route le jour J. L'inconvénient: la côte cantabrique peut être nuageuse en août.
  • +
  • 🥈 Option B — Landes/Pays Basque + road-trip: Séjourner dans un Capfun/Homair du sud des Landes, partir tôt le 12 août vers Burgos ou Vitoria. 1h30-2h de route pour 1m44s de totalité à Burgos.
  • +
  • 🥉 Option C — Intérieur espagnol (Burgos, Valladolid): Meilleure probabilité de ciel dégagé. Moins de choix de campings avec piscines/toboggans, mais totalité longue (1m44s) et ciel plus fiable.
  • +
  • 🏝️ Option D — Majorque (Baléares): Climat méditerranéen quasi garanti, campings familiaux, totalité 1m30+. Le soleil se couche vite après (20:49) mais la vue sera magnifique.
  • +
  • 🔭 Observation: La totalité dure 1 à 2 minutes. Pendant ce laps de temps, vous pouvez retirer les lunettes — c'est l'unique moment où la couronne solaire est visible à l'œil nu. AVANT et APRÈS, lunettes ISO 12312-2 obligatoires.
  • +
  • ✨ Pendant la totalité en Espagne: Vénus sera brillante dans le sud-ouest, avec Spica à côté. Arcturus haute dans le sud, le Triangle d'Été bien haut dans l'est. Mercure et Jupiter très bas sous le Soleil.
  • +
  • 🐕 Chiens: Passeport européen obligatoire pour entrer en Espagne. Carnet de vaccination à jour. Dans tous les campings listés, les chiens sont admis en laisse.
  • +
  • 🗓️ Réservez tôt ! L'éclipse totale du 12 août 2026 est l'événement astronomique de la décennie en Europe. Les campings près de la ligne de totalité seront pris d'assaut.
  • +
  • ☀️ Prochaine éclipse en Espagne: Le 2 août 2027 ! Une éclipse totale traversera le sud de l'Espagne (Andalousie, Gibraltar). Moins d'un an après — de quoi faire un rattrapage si la météo est mauvaise en 2026.
  • +
  • 👨‍👩‍👧‍👦 Pour Louis (CE1) et Maël (GS): Tous les campings proposés ont toboggans aquatiques et clubs enfants. La totalité en fin de journée (20h30) est parfaite — les enfants seront encore éveillés, et l'ambiance "nuit en plein jour" sera magique pour eux !
  • +
+
+ +
+

Éclipse solaire totale du 12 août 2026 — Données: NASA / Wikipedia · Campings: Capfun / Homair / MS Vacances / Indépendants

+

🤖 Généré par Botty — summer-2026.hidrago.click

+
+ + + \ No newline at end of file diff --git a/tokyo-trip.hidrago.click.nginx b/tokyo-trip.hidrago.click.nginx new file mode 100644 index 0000000..ed5b28b --- /dev/null +++ b/tokyo-trip.hidrago.click.nginx @@ -0,0 +1,31 @@ +server { + server_name tokyo-trip.hidrago.click; + + root /home/jerome/hermes-sites/tokyo-trip.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/tokyo-trip.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/tokyo-trip.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = tokyo-trip.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + server_name tokyo-trip.hidrago.click; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/tokyo-trip.hidrago.click/generate_trip.py b/tokyo-trip.hidrago.click/generate_trip.py new file mode 100644 index 0000000..988b521 --- /dev/null +++ b/tokyo-trip.hidrago.click/generate_trip.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +"""Generate the Tokyo Trip itinerary page (FR/EN/JA).""" + +import json +from datetime import datetime, timezone + +# ─── Trip data (easily editable) ─── +TRIP = { + "title": {"fr": "Tokyo Trip", "en": "Tokyo Trip", "ja": "東京旅行"}, + "date_label": {"fr": "Date du voyage", "en": "Trip date", "ja": "旅行日"}, + "date": "", # à remplir plus tard + "steps": [ + { + "time": "07:30", + "city": "Kuwana", + "station": {"fr": "Gare de Kuwana", "en": "Kuwana Station", "ja": "桑名駅"}, + "action": {"fr": "Départ", "en": "Departure", "ja": "出発"}, + "icon": "🚉", + "note": {"fr": "Train depuis Kuwana", "en": "Train from Kuwana", "ja": "桑名から電車で出発"}, + }, + { + "time": "08:35", + "city": "Nagoya", + "station": {"fr": "Gare de Nagoya", "en": "Nagoya Station", "ja": "名古屋駅"}, + "action": {"fr": "Arrivée à Nagoya", "en": "Arrival at Nagoya", "ja": "名古屋到着"}, + "icon": "🛬", + "note": {"fr": "22 minutes avant le Shinkansen", "en": "22 minutes before Shinkansen", "ja": "新幹線まで22分"}, + }, + { + "time": "08:57", + "city": "Nagoya", + "station": {"fr": "Gare de Nagoya", "en": "Nagoya Station", "ja": "名古屋駅"}, + "action": {"fr": "Shinkansen pour Tokyo", "en": "Shinkansen to Tokyo", "ja": "新幹線で東京へ"}, + "icon": "🚄", + "note": {"fr": "Départ en Shinkansen", "en": "Shinkansen departure", "ja": "新幹線出発"}, + }, + { + "time": "—", + "city": "Tokyo", + "station": {"fr": "Gare de Tokyo", "en": "Tokyo Station", "ja": "東京駅"}, + "action": {"fr": "Arrivée à Tokyo", "en": "Arrival at Tokyo", "ja": "東京到着"}, + "icon": "🏁", + "note": {"fr": "", "en": "", "ja": ""}, + }, + ], +} + +# ─── UI translations ─── +I18N = { + "fr": { + "title": "Tokyo Trip — Itinéraire", + "heading": "🚄 Tokyo Trip", + "subtitle": "Itinéraire de voyage", + "step": "Étape", + "time": "Heure", + "station": "Gare", + "action": "Action", + "note": "Note", + "no_date": "Date à définir", + "updated": "Mis à jour le", + }, + "en": { + "title": "Tokyo Trip — Itinerary", + "heading": "🚄 Tokyo Trip", + "subtitle": "Travel itinerary", + "step": "Step", + "time": "Time", + "station": "Station", + "action": "Action", + "note": "Note", + "no_date": "Date TBD", + "updated": "Updated on", + }, + "ja": { + "title": "東京旅行 — 旅程", + "heading": "🚄 東京旅行", + "subtitle": "旅程表", + "step": "ステップ", + "time": "時刻", + "station": "駅", + "action": "行動", + "note": "メモ", + "no_date": "日付未定", + "updated": "更新日時", + }, +} + +REMINDER = { + "fr": "📅 Rappel : le 30 juillet, départ pour Saitama ! Chichi-bouh ! 🎉", + "en": "📅 Reminder: July 30th, departure for Saitama! Chichi-bouh! 🎉", + "ja": "📅 リマインダー:7月30日、埼玉へ出発!ちちぶー!🎉", +} + +I18N_JSON = json.dumps(I18N, ensure_ascii=False) +TRIP_JSON = json.dumps(TRIP, ensure_ascii=False) +REMINDER_JSON = json.dumps(REMINDER, ensure_ascii=False) + + +def generate_html(): + now = datetime.now(timezone.utc) + + # Build step cards server-side (time, city, icon are language-independent) + steps_html = [] + for i, step in enumerate(TRIP["steps"]): + note_html = ( + f'
' + if step["note"].get("fr") + else "" + ) + steps_html.append(f""" +
+
{step['time']}
+
{step['icon']}
+
+
{step['city']}
+
+
+ {note_html} +
+
""") + + steps = "\n".join(steps_html) + # Connector arrows between steps + n_steps = len(TRIP["steps"]) + + html = f""" + + + + + Tokyo Trip — Itinéraire + + + +
+ + + +
+

🚄 Tokyo Trip

+

Itinéraire de voyage

+

+
+
+{steps} +
+
+ Mis à jour le {now.strftime('%d/%m/%Y à %H:%M UTC')} +
+ + + +""" + + return html + + +if __name__ == "__main__": + html = generate_html() + output_path = "/home/jerome/hermes-sites/tokyo-trip.hidrago.click/index.html" + with open(output_path, "w", encoding="utf-8") as f: + f.write(html) + print(f"Tokyo Trip page generated at {output_path}") diff --git a/tokyo-trip.hidrago.click/index.html b/tokyo-trip.hidrago.click/index.html new file mode 100644 index 0000000..a9e0726 --- /dev/null +++ b/tokyo-trip.hidrago.click/index.html @@ -0,0 +1,261 @@ + + + + + + Tokyo Trip — Itinéraire + + + +
+ + + +
+

🚄 Tokyo Trip

+

Itinéraire de voyage

+

+
+
+ +
+
07:30
+
🚉
+
+
Kuwana
+
+
+
+
+
+ +
+
08:35
+
🛬
+
+
Nagoya
+
+
+
+
+
+ +
+
08:57
+
🚄
+
+
Nagoya
+
+
+
+
+
+ +
+
—
+
🏁
+
+
Tokyo
+
+
+ +
+
+
+
+ Mis à jour le 25/07/2026 à 08:39 UTC +
+ + + + \ No newline at end of file diff --git a/vault.hidrago.click.nginx b/vault.hidrago.click.nginx new file mode 100644 index 0000000..42c1f4a --- /dev/null +++ b/vault.hidrago.click.nginx @@ -0,0 +1,51 @@ +server { + server_name vault.hidrago.click; + + client_max_body_size 128M; + + location /notifications/hub { + proxy_pass http://127.0.0.1:3012; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /notifications/hub/negotiate { + proxy_pass http://127.0.0.1:8081; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/vault.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/vault.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} + +server { + if ($host = vault.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + server_name vault.hidrago.click; + return 404; # managed by Certbot +} \ No newline at end of file diff --git a/vault.hidrago.click/docker-compose.yml b/vault.hidrago.click/docker-compose.yml new file mode 100644 index 0000000..151aee6 --- /dev/null +++ b/vault.hidrago.click/docker-compose.yml @@ -0,0 +1,18 @@ +# Vaultwarden — self-hosted password manager +# https://vault.hidrago.click +services: + vaultwarden: + image: vaultwarden/server:latest + container_name: vaultwarden + restart: unless-stopped + environment: + DOMAIN: "https://vault.hidrago.click" + WEBSOCKET_ENABLED: "true" + SIGNUPS_ALLOWED: "false" + # Generated via: docker run --rm -it vaultwarden/server /vaultwarden hash + ADMIN_TOKEN: "$$argon2id$$v=19$$m=65540,t=3,p=4$$ufU3+T57S9CCQLaaRKU4Y/7cZWwTWGE88IwK04lJeus$$E8AdcFA2Rltos1yTKLGn3hIU9iUHhIKLwYcqfLljSm4" + volumes: + - ./vw-data:/data + ports: + - "127.0.0.1:8081:80" + - "127.0.0.1:3012:3012" diff --git a/vault.hidrago.click/vaultwarden.service b/vault.hidrago.click/vaultwarden.service new file mode 100644 index 0000000..ca4a774 --- /dev/null +++ b/vault.hidrago.click/vaultwarden.service @@ -0,0 +1,20 @@ +[Unit] +Description=Vaultwarden - self-hosted Bitwarden-compatible password manager +Documentation=https://github.com/dani-garcia/vaultwarden +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/home/jerome/hermes-sites/vault.hidrago.click +ExecStart=/usr/bin/docker compose up -d +ExecStop=/usr/bin/docker compose down +ExecReload=/usr/bin/docker compose restart +StandardOutput=journal +StandardError=journal +TimeoutStartSec=120 +TimeoutStopSec=30 + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/weather-example.hidrago.click.nginx b/weather-example.hidrago.click.nginx new file mode 100644 index 0000000..98b751e --- /dev/null +++ b/weather-example.hidrago.click.nginx @@ -0,0 +1,31 @@ +server { + server_name weather-example.hidrago.click; + + root /home/jerome/hermes-sites/weather-example.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/weather-example.hidrago.click/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/weather-example.hidrago.click/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = weather-example.hidrago.click) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + server_name weather-example.hidrago.click; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/weather-example.hidrago.click/generate_weather.py b/weather-example.hidrago.click/generate_weather.py new file mode 100644 index 0000000..c7d3ac7 --- /dev/null +++ b/weather-example.hidrago.click/generate_weather.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +"""Fetch daily weather for Chambourcy, Kuwana, Tokyo and generate a static HTML page (FR/EN/JA).""" + +import json +import urllib.request +from datetime import datetime, timezone + +# City configs +CITIES = [ + {"name": "Chambourcy", "lat": 48.892, "lon": 2.049, "tz": "Europe/Paris"}, + {"name": "Kuwana", "lat": 35.064, "lon": 136.686, "tz": "Asia/Tokyo"}, + {"name": "Tokyo", "lat": 35.6762, "lon": 139.6503, "tz": "Asia/Tokyo"}, +] + +# WMO weather codes -> {lang: description} +WMO_CODES = { + 0: {"fr": "Ciel dégagé", "en": "Clear sky", "ja": "快晴", "emoji": "☀️"}, + 1: {"fr": "Peu nuageux", "en": "Mainly clear", "ja": "概ね晴れ", "emoji": "🌤️"}, + 2: {"fr": "Partiellement nuageux", "en": "Partly cloudy", "ja": "所により曇り", "emoji": "⛅"}, + 3: {"fr": "Couvert", "en": "Overcast", "ja": "曇り", "emoji": "☁️"}, + 45: {"fr": "Brouillard", "en": "Fog", "ja": "霧", "emoji": "🌫️"}, + 48: {"fr": "Brouillard givrant", "en": "Rime fog", "ja": "着氷霧", "emoji": "🌫️"}, + 51: {"fr": "Bruine légère", "en": "Light drizzle", "ja": "軽い霧雨", "emoji": "🌦️"}, + 53: {"fr": "Bruine modérée", "en": "Moderate drizzle", "ja": "霧雨", "emoji": "🌦️"}, + 55: {"fr": "Bruine dense", "en": "Dense drizzle", "ja": "濃い霧雨", "emoji": "🌧️"}, + 56: {"fr": "Bruine verglaçante", "en": "Freezing drizzle", "ja": "着氷霧雨", "emoji": "🌧️"}, + 57: {"fr": "Bruine verglaçante dense", "en": "Dense freezing drizzle", "ja": "濃い着氷霧雨", "emoji": "🌧️"}, + 61: {"fr": "Pluie légère", "en": "Slight rain", "ja": "小雨", "emoji": "🌧️"}, + 63: {"fr": "Pluie modérée", "en": "Moderate rain", "ja": "雨", "emoji": "🌧️"}, + 65: {"fr": "Pluie forte", "en": "Heavy rain", "ja": "大雨", "emoji": "🌧️"}, + 66: {"fr": "Pluie verglaçante", "en": "Freezing rain", "ja": "着氷雨", "emoji": "🌧️"}, + 67: {"fr": "Pluie verglaçante forte", "en": "Heavy freezing rain", "ja": "着氷大雨", "emoji": "🌧️"}, + 71: {"fr": "Neige légère", "en": "Slight snow", "ja": "小雪", "emoji": "🌨️"}, + 73: {"fr": "Neige modérée", "en": "Moderate snow", "ja": "雪", "emoji": "🌨️"}, + 75: {"fr": "Neige forte", "en": "Heavy snow", "ja": "大雪", "emoji": "❄️"}, + 77: {"fr": "Grains de neige", "en": "Snow grains", "ja": "細かい雪", "emoji": "🌨️"}, + 80: {"fr": "Averses légères", "en": "Light showers", "ja": "軽いにわか雨", "emoji": "🌦️"}, + 81: {"fr": "Averses modérées", "en": "Moderate showers", "ja": "にわか雨", "emoji": "🌧️"}, + 82: {"fr": "Averses violentes", "en": "Violent showers", "ja": "激しいにわか雨", "emoji": "⛈️"}, + 85: {"fr": "Averses de neige légères", "en": "Light snow showers", "ja": "軽いにわか雪", "emoji": "🌨️"}, + 86: {"fr": "Averses de neige fortes", "en": "Heavy snow showers", "ja": "激しいにわか雪", "emoji": "❄️"}, + 95: {"fr": "Orage", "en": "Thunderstorm", "ja": "雷雨", "emoji": "⛈️"}, + 96: {"fr": "Orage grêle léger", "en": "Thunderstorm slight hail", "ja": "雷雨・小さな雹", "emoji": "⛈️"}, + 99: {"fr": "Orage grêle fort", "en": "Thunderstorm heavy hail", "ja": "雷雨・大きな雹", "emoji": "⛈️"}, +} + +# UI translations +I18N = { + "fr": { + "title": "Météo — Chambourcy · Kuwana · Tokyo", + "heading": "🌤️ Météo du jour", + "subtitle": "Chambourcy · Kuwana · Tokyo", + "feels": "Ressenti", + "humidity": "Humidité", + "wind": "Vent", + "maxmin": "Max / Min", + "precip": "Précip.", + "sunrise": "Lever", + "sunset": "Coucher", + "data_source": "Données", + "updated": "Mis à jour le", + "error": "Erreur", + "unknown": "Inconnu", + }, + "en": { + "title": "Weather — Chambourcy · Kuwana · Tokyo", + "heading": "🌤️ Today's Weather", + "subtitle": "Chambourcy · Kuwana · Tokyo", + "feels": "Feels like", + "humidity": "Humidity", + "wind": "Wind", + "maxmin": "High / Low", + "precip": "Rain", + "sunrise": "Sunrise", + "sunset": "Sunset", + "data_source": "Data", + "updated": "Updated on", + "error": "Error", + "unknown": "Unknown", + }, + "ja": { + "title": "天気 — Chambourcy · Kuwana · Tokyo", + "heading": "🌤️ 今日の天気", + "subtitle": "Chambourcy · Kuwana · Tokyo", + "feels": "体感温度", + "humidity": "湿度", + "wind": "風速", + "maxmin": "最高 / 最低", + "precip": "降水量", + "sunrise": "日の出", + "sunset": "日の入り", + "data_source": "データ元", + "updated": "更新日時", + "error": "エラー", + "unknown": "不明", + }, +} + +# Country names per lang +COUNTRY = { + "Chambourcy": {"fr": "France", "en": "France", "ja": "フランス"}, + "Kuwana": {"fr": "Japon", "en": "Japan", "ja": "日本"}, + "Tokyo": {"fr": "Japon", "en": "Japan", "ja": "日本"}, +} + +# Export i18n + country maps as JSON for client-side JS +I18N_JSON = json.dumps(I18N, ensure_ascii=False) +COUNTRY_JSON = json.dumps(COUNTRY, ensure_ascii=False) +WMO_JSON = json.dumps(WMO_CODES, ensure_ascii=False) + + +def fetch_weather(city): + lat, lon = city["lat"], city["lon"] + url = ( + f"https://api.open-meteo.com/v1/forecast" + f"?latitude={lat}&longitude={lon}" + f"¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m" + f"&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_sum" + f"&timezone={city['tz']}" + f"&forecast_days=1" + ) + req = urllib.request.Request(url, headers={"User-Agent": "weather-example/1.0"}) + with urllib.request.urlopen(req, timeout=15) as resp: + return json.loads(resp.read().decode()) + + +def generate_html(): + now = datetime.now(timezone.utc) + cards_html = [] + + for city in CITIES: + try: + data = fetch_weather(city) + except Exception as e: + cards_html.append( + f'

{city["name"]}

' + f'

Erreur: {e}

' + ) + continue + + current = data.get("current", {}) + daily = data.get("daily", {}) + + temp = current.get("temperature_2m", "N/A") + feels = current.get("apparent_temperature", "N/A") + humidity = current.get("relative_humidity_2m", "N/A") + wind = current.get("wind_speed_10m", "N/A") + code = current.get("weather_code", 0) + + t_max = daily.get("temperature_2m_max", [None]) + t_min = daily.get("temperature_2m_min", [None]) + precip = daily.get("precipitation_sum", [None]) + sunrise = daily.get("sunrise", [None]) + sunset = daily.get("sunset", [None]) + + t_max_val = t_max[0] if t_max else "N/A" + t_min_val = t_min[0] if t_min else "N/A" + precip_val = precip[0] if precip else "N/A" + sunrise_val = sunrise[0] if sunrise else "N/A" + sunset_val = sunset[0] if sunset else "N/A" + + card = f""" +
+
+

{city['name']}

+ +
+
{temp}°C
+
+
+
Ressenti{feels}°C
+
Humidité{humidity}%
+
Vent{wind} km/h
+
Max / Min{t_max_val}° / {t_min_val}°
+
Précip.{precip_val} mm
+
Lever{sunrise_val}
+
Coucher{sunset_val}
+
+
""" + cards_html.append(card) + + cards = "\n".join(cards_html) + + html = f""" + + + + + Météo — Chambourcy · Kuwana · Tokyo + + + +
+ + + +
+

🌤️ Météo du jour

+

Chambourcy · Kuwana · Tokyo

+
+{cards} +
+
+ Données: Open-Meteo · Mis à jour le {now.strftime('%d/%m/%Y à %H:%M UTC')} +
+ + + +""" + + return html + + +if __name__ == "__main__": + html = generate_html() + output_path = "/home/jerome/hermes-sites/weather-example.hidrago.click/index.html" + with open(output_path, "w", encoding="utf-8") as f: + f.write(html) + print(f"Weather page generated at {output_path}") diff --git a/weather-example.hidrago.click/index.html b/weather-example.hidrago.click/index.html new file mode 100644 index 0000000..680560a --- /dev/null +++ b/weather-example.hidrago.click/index.html @@ -0,0 +1,246 @@ + + + + + + Météo — Chambourcy · Kuwana · Tokyo + + + +
+ + + +
+

🌤️ Météo du jour

+

Chambourcy · Kuwana · Tokyo

+
+ +
+
+

Chambourcy

+ +
+
17.2°C
+
+
+
Ressenti18.0°C
+
Humidité86%
+
Vent5.4 km/h
+
Max / Min28.3° / 17.2°
+
Précip.0.0 mm
+
Lever2026-07-31T06:23
+
Coucher2026-07-31T21:32
+
+
+ +
+
+

Kuwana

+ +
+
35.3°C
+
+
+
Ressenti41.8°C
+
Humidité56%
+
Vent9.5 km/h
+
Max / Min35.3° / 26.5°
+
Précip.0.0 mm
+
Lever2026-07-31T05:01
+
Coucher2026-07-31T18:57
+
+
+ +
+
+

Tokyo

+ +
+
32.4°C
+
+
+
Ressenti37.8°C
+
Humidité58%
+
Vent1.4 km/h
+
Max / Min32.7° / 24.5°
+
Précip.0.0 mm
+
Lever2026-07-31T04:48
+
Coucher2026-07-31T18:47
+
+
+
+
+ Données: Open-Meteo · Mis à jour le 31/07/2026 à 05:00 UTC +
+ + + + \ No newline at end of file diff --git a/yada-land.hidrago.click.nginx b/yada-land.hidrago.click.nginx new file mode 100644 index 0000000..6a50587 --- /dev/null +++ b/yada-land.hidrago.click.nginx @@ -0,0 +1,26 @@ +server { + server_name yada-land.hidrago.click; + + root /home/jerome/hermes-sites/yada-land.hidrago.click; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/hidrago.click/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/hidrago.click/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +} + +server { + if ($host = yada-land.hidrago.click) { + return 301 https://$host$request_uri; + } + + listen 80; + server_name yada-land.hidrago.click; + return 404; +} diff --git a/yada-land.hidrago.click/index.html b/yada-land.hidrago.click/index.html new file mode 100644 index 0000000..4a98bd7 --- /dev/null +++ b/yada-land.hidrago.click/index.html @@ -0,0 +1,945 @@ + + + + + + Yada Land — 桑名市 矢田の土地価格 + + + +
+
+

🏠 Yada Land

+
Prix des terrains à construire — Kuwana, préfecture de Mie
+
桑名市 矢田 の土地価格情報
+
📍 147-2 Yada, Kuwana, Mie 🇯🇵
+
+ +
+ + +
+ + +
+
+

📊 Résumé — 桑名市 (données 2026)

+
桑名市の土地価格まとめ(2026年)
+
+ +
+
+
Prix moyen / 平均地価
FR: tous types · JP: 全用途
+
57,356 ¥/m²
+
+
+
Terrain résidentiel / 住宅地
FR: moyen · JP: 平均
+
54,378 ¥/m²
+
+
+
Prix au tsubo / 坪単価
+
~190,000 ¥/坪
+
+
+
Tendance / 変動率
+
+0.79%/an ↑
+
+
+
+ + +
+
+

Prix par type de zone

+
用途地域別の土地価格
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FR / JP¥/m²¥/坪FR: Tendance · JP: 変動率
住宅地 · Résidentiel54,378179,765+0.72%
商業地 · Commercial87,771290,153+0.99%
工業地 · Industriel28,65094,710+1.40%
+

Source: 公示地価 2026 (tochidai.info) · MLIT 公示地価

+
+ + +
+
+

🚉 Prix par gare (autour de Yada)

+
矢田周辺の駅別地価
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FR: Gare · JP: 駅名¥/m²¥/坪変動率FR: Distance Yada · JP: 矢田からの距離
馬道 (Umamichi)100,000330,578+2.88%~1 km
桑名 (Kuwana)89,515295,918+1.87%~700 m
益生 (Masuo)60,787200,950+0.85%~1.5 km
星川 (Hoshikawa)59,720197,421+0.79%~2 km
下深谷 (Shimofukaya)59,100195,371+1.03%~3 km
播磨 (Harima)53,800177,851+0.84%~3 km
伊勢朝日 (Ise-Asahi)47,650157,520+0.57%~4 km
蓮花寺 (Rengeji)39,883131,845+0.72%~5 km
長島 (Nagashima)30,550100,991-0.87%~8 km
+

Source: 公示地価 2026 — 駅地価ランキング

+
+ + +
+
+

Ton quartier : 矢田 (Yada)

+
矢田(やだ)の地価情報
+
+ +
+

Le point de référence officiel le plus proche de chez toi :

+
+ +

大字矢田字大塚90番2外

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FR: Prix officiel · JP: 公示地価102,000 ¥/m²
FR: Prix au tsubo · JP: 坪単価337,190 ¥/坪
FR: Tendance · JP: 変動率+3.98% ↑ (en forte hausse)
FR: Distance gare Kuwana · JP: 桑名駅からの距離660 m
FR: Zone · JP: 用途地域第一種住居地域 · Residential zone
FR: Emprise au sol · JP: 建蔽率60%
FR: Coefficient d'occupation · JP: 容積率200%
FR: Superficie de référence · JP: 地積184 m² (55 坪)
+

Source: 公示地価 2026 — 地点ランキング 4位/桑名市

+
+ + +
+
+

📈 Évolution des prix sur 10 ans (2016–2026)

+
過去10年間の地価推移(2016〜2026年)
+

Évolution de la moyenne des prix officiels (公示地価) à 桑名市, tous types confondus :

+
+ + +
+
+
+ 65,000 + 60,000 + 55,000 + 50,000 + 45,000 +
+
+
+
+ FR: Prix moyen (¥/m²) · JP: 平均地価(円/m²) +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
FR: Année · JP: 年FR: ¥/m² · JP: 円/m²FR: ¥/坪 · JP: 円/坪FR: Variation · JP: 変動率FR: vs 2016 · JP: 2016年比
201654,300179,504-0.61%—
201753,404176,542-0.54%-1.65%
201853,371176,434-0.37%-1.71%
201954,002178,519-0.26%-0.55%
202054,602180,502-0.03%+0.56%
202154,381179,773-0.59%+0.15%
202254,324179,585-0.31%+0.04%
202355,428183,234+0.14%+2.08%
202455,820184,530+0.62%+2.80%
202556,635187,224+0.85%+4.30%
202657,356189,607+0.79%+5.63%
+ +
+

📊 Analyse / 分析:

+
    +
  • 2016–2022 : Léger déclin (~-1.7%), tendance baissière héritée du post-bulle
  • +
  • 2020 : Point bas, stabilisation autour de 54,300 ¥/m²
  • +
  • 2023–2026 : Retournement à la hausse, +5.6% cumulé depuis le point bas
  • +
  • Yada (矢田) spécifiquement : +3.98%/an — bien supérieur à la moyenne de la ville
  • +
  • La hausse récente coïncide avec l'avancée des travaux de la Route 1 (国道1号桑名東部拡幅)
  • +
+
+ +

Source: 公示地価 2016–2026 (tochidai.info) · 国土交通省 MLIT

+
+ + +
+
+

🚧 Projet : Élargissement Route 1 — 国道1号桑名東部拡幅事業

+
国道1号桑名東部拡幅事業について
+
+ + +
+
+
FR: Tracé · JP: 事業区間
+
長島町又木 ~ 北浜町
+
+
+
FR: Longueur · JP: 延長
+
3.9 km
+
+
+
FR: Objectif · JP: 目的
+
4車線化 + 伊勢大橋架け替え
+
+
+
FR: Début travaux · JP: 着工
+
2015年9月 (平成27年)
+
+
+
FR: Maître d'ouvrage · JP: 主体
+
国土交通省 北勢国道事務所
+
+
+
FR: Statut · JP: 状況
+
🚧 工事進行中 (15橋脚中11完成)
+
+
+ +
+

📌 Pourquoi ce projet ?

+

1. Pont vieillissant : L'伊勢大橋 (Ise-ōhashi) a été construit en 1934 (90+ ans). Détérioré par la pluie et le sel, il est vulnérable aux tsunami (南海トラフ巨大地震).

+

2. Embouteillages : C'est le seul secteur de Kuwana classé "congestion majeure", avec 3 carrefours problématiques. Le temps perdu par km est 6,4× la moyenne préfectorale de Mie.

+

3. Sécurité : Ajout de voies de virage à droite, élargissement à 4 voies, nouvelles normes antisismiques pour le pont.

+ +

📍 Impact sur Yada (矢田)

+

La Route 1 passe par le centre de Kuwana, tout près de la gare et de ton quartier 矢田. Le point de référence officiel à 大字矢田字大塚 est à ~660m de la gare, directement sur l'axe du projet.

+
+ + +
+
+
+

⚠️ Pendant les travaux / 工事中

+
+ +
    +
  • Déviations et circulation réduite
  • +
  • Bruit, poussière, vibrations
  • +
  • Acquisitions foncières (移転補償)
  • +
  • Gêne de circulation pendant 10+ ans
  • +
+
+
+
+

✅ Après achèvement / 完成後

+
+ +
    +
  • 4 voies → trafic fluide
  • +
  • Nouveau pont antisismique
  • +
  • Voie de fuite en cas de tsunami
  • +
  • Valorisation des terrains proches
  • +
+
+
+ +
+

💡 Impact sur les prix : La hausse de +3.98%/an à 矢田 (vs +0.79% pour la moyenne de 桑名市) suggère que le marché anticipe déjà les bénéfices de l'élargissement. Les terrains proches de la Route 1 pourraient continuer à augmenter à mesure que les travaux avancent.

+
+ + +

Source: 国土交通省 中部地方整備局 北勢国道事務所 · 桑名市公式サイト

+
+ + +
+
+

💰 Estimation budget pour construire

+
建築用土地の予算目安
+

Budget indicatif pour acheter un terrain constructible autour de Yada :

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FR: Surface · JP: 面積FR: Zone Yada/Gare · JP: 矢田・駅周辺FR: Zone éloignée · JP: 郊外
30 坪 (~100 m²)7.5M – 10.5M ¥4.5M – 6M ¥
50 坪 (~165 m²)12.5M – 17.5M ¥7.5M – 10M ¥
80 坪 (~265 m²)20M – 28M ¥12M – 16M ¥
100 坪 (~330 m²)25M – 35M ¥15M – 20M ¥
+

FR: Estimations basées sur les prix officiels et transactions réelles · JP: 公示地価・実取引価格に基づく概算

+
+ + +
+
+

📋 Transactions réelles (2023)

+
実際の取引価格(2023年)
+
+ + + + + + + + + + + + + + + + + + + + +
FR: Nombre de transactions · JP: 取引件数183件
FR: Prix moyen réel · JP: 平均取引価格44,682 ¥/m²
FR: Soit au tsubo · JP: 坪単価~147,580 ¥/坪
FR: vs prix officiel · JP: 公示地価との差~18% en dessous / 約18%低い
+

Source: 国土交通省 API — 不動産取引価格 (land-cost.work)

+
+ + +
+
+

🏆 Top 5 des points les plus chers (桑名市)

+
桑名市の地価上位5地点
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FR: Adresse · JP: 住所¥/m²¥/坪変動率
1寿町2丁目 (Kuwana gare)278,000919,008+3.73%
2末広町58番外159,000525,619+2.58%
3中央町4丁目132,000436,363+2.33%
4大字矢田字大塚 (Yada 📍)102,000337,190+3.98%
5松並町2丁目102,000337,190+3.13%
+

Source: 公示地価 2026 & 基準地価 2025 — 地点ランキング

+
+ +
+

FR: Données: tochidai.info · land-cost.work · SUUMO · 国土交通省 (MLIT) · 桑名市 — Juillet 2026

+

JP: データ出典:tochidai.info · land-cost.work · SUUMO · 国土交通省 · 桑名市 — 2026年7月

+

🏠 Generated by Botty for Yada, Kuwana — 矢田のための土地情報

+
+
+ + + +