Administration depuis le site : bouton Connexion sur l'accueil
site.toml [administration] adresse : l'accueil affiche un bouton « Connexion » vers l'interface. Celle-ci peut tourner derrière le serveur web du site (rtg interface --proxy --prefixe /admin) : adresses préfixées, mot de passe obligatoire, visiteur identifié par X-Forwarded-For, cookie Secure limité au préfixe quand le site est en HTTPS. Exemples nginx et Apache dans le README. Le bouton Arrêter ne remplace plus le bloc de la tâche en même temps que la relève. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
1 parent
71779f6f53
commit
4366db94ed
22 files changed
+229
-86
No files matched your search
@@ -16,7 +16,7 @@
|
||||
<tr>
|
||||
<td><code>{{ nom }}</code> <span class="discret">{{ d.km|round|int }} km</span></td>
|
||||
<td>
|
||||
<form class="en-ligne" hx-post="/voyage/date" hx-target="#analyse">
|
||||
<form class="en-ligne" hx-post="{{ base }}/voyage/date" hx-target="#analyse">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}"><input type="hidden" name="fichier" value="{{ nom }}">
|
||||
<input type="date" name="jour" required><button>OK</button>
|
||||
</form>
|
||||
@@ -24,7 +24,7 @@
|
||||
<td>
|
||||
{% set s = suggestions.get(nom) %}
|
||||
{% if s and s[0] %}
|
||||
<button class="petit" hx-post="/voyage/date" hx-target="#analyse"
|
||||
<button class="petit" hx-post="{{ base }}/voyage/date" hx-target="#analyse"
|
||||
hx-vals='{{ {"v": v.chemin, "fichier": nom, "jour": s[0].isoformat()}|tojson }}'
|
||||
title="{{ s[1] }} photo(s) ou vidéo(s) de ce jour prises le long de la trace">Le {{ s[0]|date_longue }} ?</button>
|
||||
{% else %}<span class="discret">pas de suggestion</span>{% endif %}
|
||||
@@ -34,7 +34,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% if non_datees|length > 1 %}
|
||||
<form class="en-ligne" hx-post="/voyage/dates-suite" hx-target="#analyse">
|
||||
<form class="en-ligne" hx-post="{{ base }}/voyage/dates-suite" hx-target="#analyse">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||
Ou bien, dans l'ordre des noms, un jour par trace à partir du <input type="date" name="debut" required>
|
||||
<button>Répartir</button>
|
||||
@@ -66,13 +66,13 @@
|
||||
<td><code title="{{ d.fichier }}">{{ d.nom }}</code>{% if d.erreur %} <span class="pastille attention" title="{{ d.erreur }}">illisible</span>{% endif %}</td>
|
||||
<td>{% for j in d.jours %}{{ j|date_courte }}{{ ", " if not loop.last }}{% else %}<span class="discret">—</span>{% endfor %}</td>
|
||||
<td>
|
||||
<form class="en-ligne" hx-post="/voyage/date" hx-target="#analyse">
|
||||
<form class="en-ligne" hx-post="{{ base }}/voyage/date" hx-target="#analyse">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}"><input type="hidden" name="fichier" value="{{ d.nom }}">
|
||||
<input type="date" name="jour" value="{{ d.impose.isoformat() if d.impose else '' }}">
|
||||
<button class="petit">{{ "Changer" if d.impose else "Imposer" }}</button>
|
||||
</form>
|
||||
{% if d.impose %}
|
||||
<button class="petit" hx-post="/voyage/date" hx-target="#analyse"
|
||||
<button class="petit" hx-post="{{ base }}/voyage/date" hx-target="#analyse"
|
||||
hx-vals='{{ {"v": v.chemin, "fichier": d.nom, "jour": ""}|tojson }}'>Annuler</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% if tache and tache.en_cours %}
|
||||
<a class="bandeau en-cours" href="/tache">⏳ {{ tache.libelle }} · {{ tache.etape }}{% if tache.etape_voyage %} ({{ tache.etape_voyage }}){% endif %}{% if tache.video and tache.video.rang %} · vidéo {{ tache.video.rang[0] }}/{{ tache.video.rang[1] }} ({{ tache.video.pct|int }} %){% endif %}</a>
|
||||
<a class="bandeau en-cours" href="{{ base }}/tache">⏳ {{ tache.libelle }} · {{ tache.etape }}{% if tache.etape_voyage %} ({{ tache.etape_voyage }}){% endif %}{% if tache.video and tache.video.rang %} · vidéo {{ tache.video.rang[0] }}/{{ tache.video.rang[1] }} ({{ tache.video.pct|int }} %){% endif %}</a>
|
||||
{% elif tache %}
|
||||
<a class="bandeau {{ 'ok' if tache.ok else 'echec' }}" href="/tache">{{ "✓" if tache.ok else "⚠️" }} {{ tache.libelle }} : {{ tache.etape|lower }}</a>
|
||||
<a class="bandeau {{ 'ok' if tache.ok else 'echec' }}" href="{{ base }}/tache">{{ "✓" if tache.ok else "⚠️" }} {{ tache.libelle }} : {{ tache.etape|lower }}</a>
|
||||
{% endif %}
|
||||
@@ -3,14 +3,14 @@
|
||||
<h3>Choisir un dossier de {{ libelle }}</h3>
|
||||
<div class="raccourcis">
|
||||
{% for nom, c in raccourcis %}
|
||||
<button class="petit" hx-get="/dossiers?cle={{ cle }}&v={{ v|url }}&chemin={{ c|url }}" hx-target="#fenetre">{{ nom }}</button>
|
||||
<button class="petit" hx-get="{{ base }}/dossiers?cle={{ cle }}&v={{ v|url }}&chemin={{ c|url }}" hx-target="#fenetre">{{ nom }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="chemin"><code>{{ chemin }}</code></p>
|
||||
<ul class="liste-dossiers">
|
||||
{% if parent %}<li><a href="#" hx-get="/dossiers?cle={{ cle }}&v={{ v|url }}&chemin={{ parent|url }}" hx-target="#fenetre">⬆︎ Dossier parent</a></li>{% endif %}
|
||||
{% if parent %}<li><a href="#" hx-get="{{ base }}/dossiers?cle={{ cle }}&v={{ v|url }}&chemin={{ parent|url }}" hx-target="#fenetre">⬆︎ Dossier parent</a></li>{% endif %}
|
||||
{% for d in sous %}
|
||||
<li><a href="#" hx-get="/dossiers?cle={{ cle }}&v={{ v|url }}&chemin={{ (chemin.rstrip(sep) ~ sep ~ d)|url }}" hx-target="#fenetre">📁 {{ d }}</a></li>
|
||||
<li><a href="#" hx-get="{{ base }}/dossiers?cle={{ cle }}&v={{ v|url }}&chemin={{ (chemin.rstrip(sep) ~ sep ~ d)|url }}" hx-target="#fenetre">📁 {{ d }}</a></li>
|
||||
{% else %}
|
||||
<li class="vide">Pas de sous-dossier</li>
|
||||
{% endfor %}
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="{{ 'ok' if nb else 'discret' }}">{{ nb }} fichier{{ 's' if nb > 1 }} de {{ libelle }} ici</span>
|
||||
<span>
|
||||
<button data-fermer>Annuler</button>
|
||||
<button class="principal" data-fermer hx-post="/voyage/sources" hx-target="#sources" hx-swap="outerHTML"
|
||||
<button class="principal" data-fermer hx-post="{{ base }}/voyage/sources" hx-target="#sources" hx-swap="outerHTML"
|
||||
hx-vals='{{ {"v": v, "cle": cle, "action": "ajouter", "chemin": chemin}|tojson }}'>Choisir ce dossier</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
<div><span class="etiquette">Encodage</span>
|
||||
{{ noms_qualites.get(cfg.video.qualite, cfg.video.qualite) }} · {{ "matériel" if i.encodage.encodeur == "vt" else "processeur" }}
|
||||
<span class="discret">(HEVC CRF {{ i.encodage.crf_hevc }} / H.264 CRF {{ i.encodage.crf_h264 }})</span>
|
||||
<a href="/reglages#video">changer</a></div>
|
||||
<a href="{{ base }}/reglages#video">changer</a></div>
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@
|
||||
{% if l.existe %}<span class="pastille {{ 'ok' if l.nb else 'neutre' }}">{{ l.nb }} fichier{{ 's' if l.nb > 1 }}</span>
|
||||
{% elif l.lien_casse %}<span class="pastille attention">disque débranché ?</span>
|
||||
{% else %}<span class="pastille attention">introuvable</span>{% endif %}
|
||||
<button class="petit" hx-post="/voyage/sources" hx-target="#sources" hx-swap="outerHTML"
|
||||
<button class="petit" hx-post="{{ base }}/voyage/sources" hx-target="#sources" hx-swap="outerHTML"
|
||||
hx-vals='{{ {"v": v.chemin, "cle": cle, "action": "retirer", "chemin": l.source}|tojson }}'>Retirer</button>
|
||||
</li>
|
||||
{% else %}
|
||||
@@ -17,8 +17,8 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="ajout">
|
||||
<button hx-get="/dossiers?cle={{ cle }}&v={{ v.chemin|url }}" hx-target="#fenetre">Choisir un dossier…</button>
|
||||
<form hx-post="/voyage/sources" hx-target="#sources" hx-swap="outerHTML" class="en-ligne">
|
||||
<button hx-get="{{ base }}/dossiers?cle={{ cle }}&v={{ v.chemin|url }}" hx-target="#fenetre">Choisir un dossier…</button>
|
||||
<form hx-post="{{ base }}/voyage/sources" hx-target="#sources" hx-swap="outerHTML" class="en-ligne">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||
<input type="hidden" name="cle" value="{{ cle }}">
|
||||
<input type="hidden" name="action" value="ajouter">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% if message %}<div class="erreur">{{ message }}</div>{% endif %}
|
||||
{% if tache %}
|
||||
<div class="tache {{ 'en-cours' if tache.en_cours else ('ok' if tache.ok else 'echec') }}"
|
||||
{% if tache.en_cours %}hx-get="/tache/fragment{{ '?complet=1' if complet }}" hx-trigger="every 1s" hx-swap="outerHTML"{% endif %}>
|
||||
{% if tache.en_cours %}hx-get="{{ base }}/tache/fragment{{ '?complet=1' if complet }}" hx-trigger="every 1s" hx-swap="outerHTML"{% endif %}>
|
||||
<div class="tache-tete">
|
||||
<strong>{{ tache.libelle }}</strong>
|
||||
<span class="etape">{{ "⏳ " if tache.en_cours }}{{ tache.etape }}{% if tache.etape_voyage %} — {{ tache.etape_voyage }}{% endif %}</span>
|
||||
<span class="discret">{{ tache.duree }}</span>
|
||||
{% if tache.en_cours %}
|
||||
<button class="danger petit" hx-post="/tache/arreter" hx-target="closest .tache" hx-swap="outerHTML">Arrêter</button>
|
||||
<button class="danger petit" hx-post="{{ base }}/tache/arreter" hx-swap="none">Arrêter</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if tache.video and tache.video.rang %}
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block titre %}{% endblock %} · RTG</title>
|
||||
<link rel="stylesheet" href="/static/rtg.css">
|
||||
<link rel="stylesheet" href="{{ base }}/static/rtg.css">
|
||||
{% block tete %}{% endblock %}
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="{{ base }}/static/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body data-base="{{ base }}">
|
||||
<header class="barre">
|
||||
<a class="logo" href="/">RTG</a>
|
||||
<a class="logo" href="{{ base }}/">RTG</a>
|
||||
<nav>
|
||||
<a href="/">Mes voyages</a>
|
||||
<a href="/reglages">Réglages</a>
|
||||
<a href="/tache">Traitement</a>
|
||||
<a href="/site/" target="_blank">Voir le site ↗</a>
|
||||
<a href="{{ base }}/">Mes voyages</a>
|
||||
<a href="{{ base }}/reglages">Réglages</a>
|
||||
<a href="{{ base }}/tache">Traitement</a>
|
||||
<a href="{{ lien_site }}" target="_blank">Voir le site ↗</a>
|
||||
</nav>
|
||||
<div id="bandeau" hx-get="/tache/bandeau" hx-trigger="load, every 2s"></div>
|
||||
<button class="petit discret-bouton" hx-post="/deconnexion" title="Fermer la session de ce navigateur">Se déconnecter</button>
|
||||
<div id="bandeau" hx-get="{{ base }}/tache/bandeau" hx-trigger="load, every 2s"></div>
|
||||
<button class="petit discret-bouton" hx-post="{{ base }}/deconnexion" title="Fermer la session de ce navigateur">Se déconnecter</button>
|
||||
</header>
|
||||
<main>
|
||||
{% block contenu %}{% endblock %}
|
||||
@@ -26,7 +26,7 @@
|
||||
<div id="messages" aria-live="polite"></div>
|
||||
<div id="fenetre"></div>
|
||||
<footer>RoadTripsGenerator {{ version }} · <code>{{ racine }}</code></footer>
|
||||
<script src="/static/rtg.js"></script>
|
||||
<script src="{{ base }}/static/rtg.js"></script>
|
||||
{% block fin %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,15 +4,15 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Connexion · RTG</title>
|
||||
<link rel="stylesheet" href="/static/rtg.css">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link rel="stylesheet" href="{{ base }}/static/rtg.css">
|
||||
<script src="{{ base }}/static/htmx.min.js"></script>
|
||||
</head>
|
||||
<body class="page-connexion">
|
||||
<main class="connexion">
|
||||
<p class="logo">RTG</p>
|
||||
{% if connecte %}
|
||||
<p>Vous êtes connecté.</p>
|
||||
<p><a class="bouton principal" href="/">Ouvrir l'interface</a></p>
|
||||
<p><a class="bouton principal" href="{{ base }}/">Ouvrir l'interface</a></p>
|
||||
{% else %}
|
||||
<h1>Accès à l'interface</h1>
|
||||
{% if not distant %}
|
||||
@@ -20,7 +20,7 @@
|
||||
elle contient la clé d'accès de cette session{% if mdp %}, ou entrez le mot de passe{% endif %}.</p>
|
||||
{% endif %}
|
||||
{% if mdp %}
|
||||
<form hx-post="/connexion" hx-target="#retour-connexion">
|
||||
<form hx-post="{{ base }}/connexion" hx-target="#retour-connexion">
|
||||
<label>Mot de passe
|
||||
<input type="password" name="mot_de_passe" autocomplete="current-password" required autofocus>
|
||||
</label>
|
||||
@@ -32,6 +32,7 @@
|
||||
<code>rtg mot-de-passe</code>.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if site %}<p class="aide"><a href="{{ site }}">← Retour au site</a></p>{% endif %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block titre %}Nouveau voyage{% endblock %}
|
||||
{% block contenu %}
|
||||
<h1>Nouveau voyage</h1>
|
||||
<form id="formulaire" class="formulaire" hx-post="/nouveau" hx-target="#formulaire" hx-select="#formulaire" hx-swap="outerHTML">
|
||||
<form id="formulaire" class="formulaire" hx-post="{{ base }}/nouveau" hx-target="#formulaire" hx-select="#formulaire" hx-swap="outerHTML">
|
||||
{% if erreur %}<div class="erreur">{{ erreur }}</div>{% endif %}
|
||||
<label>Titre
|
||||
<input name="titre" value="{{ form.titre[0] if form and form.titre else '' }}" placeholder="Été 2027 en Écosse" autofocus>
|
||||
@@ -22,7 +22,7 @@
|
||||
{% endfor %}
|
||||
<label class="choix">Nouvelle rubrique : <input name="groupe_nouveau" placeholder="Balades"></label>
|
||||
</fieldset>
|
||||
<p><button class="principal">Créer le voyage</button> <a href="/">Annuler</a></p>
|
||||
<p><button class="principal">Créer le voyage</button> <a href="{{ base }}/">Annuler</a></p>
|
||||
<p class="aide">Le dossier est créé avec un <code>voyage.toml</code>. Vous indiquerez ensuite où sont les traces, les vidéos et les photos : elles restent où elles sont.</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block contenu %}
|
||||
<h1>Réglages du site</h1>
|
||||
<p class="aide">Enregistrés dans <code>site.toml</code>. Ils valent pour tous les voyages.</p>
|
||||
<form class="formulaire" hx-post="/reglages" hx-target="#retour-reglages">
|
||||
<form class="formulaire" hx-post="{{ base }}/reglages" hx-target="#retour-reglages">
|
||||
<fieldset>
|
||||
<legend>Site</legend>
|
||||
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
||||
@@ -60,9 +60,15 @@
|
||||
<textarea name="mymaps" rows="3">{{ cfg.carte_globale.mymaps|join("\n") }}</textarea>
|
||||
<span class="aide">Leurs tracés sont récupérés une fois et ajoutés à la carte de l'accueil, sur fond OpenStreetMap.</span></label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Administration depuis le site</legend>
|
||||
<label>Adresse de l'interface <input name="administration" value="{{ cfg.administration.adresse }}" placeholder="/admin/">
|
||||
<span class="aide">Si elle est remplie, l'accueil du site affiche un bouton « Connexion » vers cette adresse.
|
||||
L'interface doit alors tourner sur le serveur du site (<code>rtg interface --proxy --prefixe /admin</code>), derrière le serveur web en HTTPS : voir le README.</span></label>
|
||||
</fieldset>
|
||||
<p><button class="principal">Enregistrer</button> <span id="retour-reglages"></span></p>
|
||||
</form>
|
||||
|
||||
<h2>Système</h2>
|
||||
<div hx-get="/systeme" hx-trigger="load"><p class="chargement">Vérification des outils…</p></div>
|
||||
<div hx-get="{{ base }}/systeme" hx-trigger="load"><p class="chargement">Vérification des outils…</p></div>
|
||||
{% endblock %}
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block contenu %}
|
||||
<h1>Traitement</h1>
|
||||
{% if tache %}
|
||||
{% if tache.voyage %}<p><a href="/voyage?v={{ tache.voyage|url }}">← {{ tache.voyage }}</a></p>{% endif %}
|
||||
{% if tache.voyage %}<p><a href="{{ base }}/voyage?v={{ tache.voyage|url }}">← {{ tache.voyage }}</a></p>{% endif %}
|
||||
{% with complet = True %}{% include "_tache.html" %}{% endwith %}
|
||||
{% else %}
|
||||
<p class="vide">Aucun traitement lancé depuis l'ouverture de l'interface.</p>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}{{ v.titre }}{% endblock %}
|
||||
{% block tete %}<link rel="stylesheet" href="/static/leaflet.css"><script src="/static/leaflet.js"></script>{% endblock %}
|
||||
{% block fin %}<script src="/static/editeur.js"></script>{% endblock %}
|
||||
{% block tete %}<link rel="stylesheet" href="{{ base }}/static/leaflet.css"><script src="{{ base }}/static/leaflet.js"></script>{% endblock %}
|
||||
{% block fin %}<script src="{{ base }}/static/editeur.js"></script>{% endblock %}
|
||||
{% block contenu %}
|
||||
<div class="entete-page">
|
||||
<div>
|
||||
<p class="fil"><a href="/">Mes voyages</a> › {{ v.rubrique }}</p>
|
||||
<p class="fil"><a href="{{ base }}/">Mes voyages</a> › {{ v.rubrique }}</p>
|
||||
<h1>{{ v.titre }}</h1>
|
||||
<p class="discret">{{ [periode(v.debut, v.fin), v.moto]|select|join(" · ") }} <code>{{ v.chemin }}</code></p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{% if v.genere %}<a class="bouton" href="/site/{{ v.chemin }}/index.html" target="_blank">Voir la page ↗</a>{% endif %}
|
||||
{% if v.genere %}<a class="bouton" href="{{ base }}/site/{{ v.chemin }}/index.html" target="_blank">Voir la page ↗</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</nav>
|
||||
|
||||
<section class="onglet" id="onglet-journees">
|
||||
<div id="analyse" hx-get="/voyage/analyse?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body">
|
||||
<div id="analyse" hx-get="{{ base }}/voyage/analyse?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body">
|
||||
<p class="chargement">Lecture des traces…</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -71,14 +71,14 @@
|
||||
</section>
|
||||
|
||||
<section class="onglet" id="onglet-infos">
|
||||
<form class="formulaire" hx-post="/voyage/infos" hx-target="find .retour">
|
||||
<form class="formulaire" hx-post="{{ base }}/voyage/infos" hx-target="find .retour">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
||||
<label>Moto <input name="moto" value="{{ cfg.moto }}" placeholder="facultatif : affichée dans la légende de la carte globale"></label>
|
||||
<p class="aide">Rubrique : <strong>{{ v.rubrique }}</strong> (elle vient du dossier où est rangé le voyage).</p>
|
||||
<p><button class="principal">Enregistrer</button> <span class="retour"></span></p>
|
||||
</form>
|
||||
<form class="formulaire large" hx-post="/voyage/recit" hx-target="find .retour">
|
||||
<form class="formulaire large" hx-post="{{ base }}/voyage/recit" hx-target="find .retour">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||
<label>Récit (<code>voyage.md</code>, en Markdown)
|
||||
<textarea name="recit" rows="18" spellcheck="true">{{ recit }}</textarea>
|
||||
@@ -90,8 +90,8 @@
|
||||
</section>
|
||||
|
||||
<section class="onglet" id="onglet-generer">
|
||||
<div hx-get="/voyage/infos-utiles?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body"><p class="chargement">Calcul des tailles…</p></div>
|
||||
<form class="formulaire" hx-post="/tache/lancer" hx-target="#tache-zone">
|
||||
<div hx-get="{{ base }}/voyage/infos-utiles?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body"><p class="chargement">Calcul des tailles…</p></div>
|
||||
<form class="formulaire" hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||
<label class="choix"><input type="checkbox" name="sans_compression"> Sans encoder les vidéos <span class="aide">(photos, positions et pages seulement)</span></label>
|
||||
<label class="choix"><input type="checkbox" name="force"> Tout refaire, même ce qui est déjà à jour</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% if not site_existe %}
|
||||
<div class="encart">
|
||||
<p><strong>Ce dossier n'est pas encore un site RTG.</strong> Il n'y a pas de <code>site.toml</code> dans <code>{{ racine }}</code>.</p>
|
||||
<p><button class="principal" hx-post="/site/creer">Créer le site dans ce dossier</button></p>
|
||||
<p><button class="principal" hx-post="{{ base }}/site/creer">Créer le site dans ce dossier</button></p>
|
||||
<p class="aide">Pour un autre dossier, relancez avec <code>rtg interface --racine CHEMIN</code>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -12,8 +12,8 @@
|
||||
<div class="entete-page">
|
||||
<h1>Mes voyages <span class="compte">{{ nb }}</span></h1>
|
||||
<div class="actions">
|
||||
<a class="bouton principal" href="/nouveau">+ Nouveau voyage</a>
|
||||
<form hx-post="/tache/lancer" hx-target="#tache-zone">
|
||||
<a class="bouton principal" href="{{ base }}/nouveau">+ Nouveau voyage</a>
|
||||
<form hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
||||
<input type="hidden" name="quoi" value="accueil">
|
||||
<button title="index.html, pages des rubriques et carte globale">Refaire l'accueil</button>
|
||||
</form>
|
||||
@@ -29,7 +29,7 @@
|
||||
<tbody>
|
||||
{% for v in liste %}
|
||||
<tr>
|
||||
<td><a href="/voyage?v={{ v.chemin|url }}"><strong>{{ v.titre }}</strong></a>
|
||||
<td><a href="{{ base }}/voyage?v={{ v.chemin|url }}"><strong>{{ v.titre }}</strong></a>
|
||||
{% if v.moto %}<div class="discret">{{ v.moto }}</div>{% endif %}</td>
|
||||
<td>{{ periode(v.debut, v.fin) }}</td>
|
||||
<td class="num">{{ v.jours or "—" }}</td>
|
||||
@@ -51,7 +51,7 @@
|
||||
{% else %}<span class="pastille attention">à refaire</span>{% endif %}
|
||||
{% if v.a_verifier %}<span class="pastille attention" title="{{ v.a_verifier|join('\n') }}">⚠️ {{ v.a_verifier|length }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="droite">{% if v.genere %}<a href="/site/{{ v.chemin }}/index.html" target="_blank">Voir ↗</a>{% endif %}</td>
|
||||
<td class="droite">{% if v.genere %}<a href="{{ base }}/site/{{ v.chemin }}/index.html" target="_blank">Voir ↗</a>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -24,10 +24,11 @@ ESSAIS_LIBRES = 5
|
||||
|
||||
|
||||
class Gardien:
|
||||
def __init__(self, port, distant=False, https=False):
|
||||
def __init__(self, port, distant=False, https=False, proxy=False, prefixe=""):
|
||||
self.jeton = secrets.token_urlsafe(24)
|
||||
self.cookie = f"rtg_{port}"
|
||||
self.distant, self.https = distant, https
|
||||
self.distant, self.https, self.proxy = distant, https, proxy
|
||||
self.chemin_cookie = prefixe + "/" if prefixe else "/"
|
||||
self._sessions = {}
|
||||
self._echecs = {} # ip → (nombre d'échecs, heure du dernier)
|
||||
self._verrou = threading.Lock()
|
||||
@@ -55,11 +56,11 @@ class Gardien:
|
||||
with self._verrou:
|
||||
self._sessions.pop(sid, None)
|
||||
|
||||
def entete_cookie(self, sid, effacer=False):
|
||||
valeur = f"{self.cookie}={'' if effacer else sid}; Path=/; HttpOnly; SameSite=Strict"
|
||||
def entete_cookie(self, sid, https=None, effacer=False):
|
||||
valeur = f"{self.cookie}={'' if effacer else sid}; Path={self.chemin_cookie}; HttpOnly; SameSite=Strict"
|
||||
if effacer:
|
||||
valeur += "; Max-Age=0"
|
||||
if self.https:
|
||||
if self.https if https is None else https:
|
||||
valeur += "; Secure"
|
||||
return valeur
|
||||
|
||||
|
||||
+67
-26
@@ -99,8 +99,12 @@ def json_rep(donnees):
|
||||
|
||||
# --- Application ---------------------------------------------------------------
|
||||
class Application:
|
||||
def __init__(self, racine):
|
||||
def __init__(self, racine, prefixe=""):
|
||||
self.racine = os.path.abspath(racine)
|
||||
# Préfixe d'adresse quand l'interface est servie sous le site
|
||||
# (https://…/admin/ derrière le serveur web) : « /admin ».
|
||||
self.prefixe = ("/" + prefixe.strip("/")) if prefixe.strip("/") else ""
|
||||
ENV.globals["base"] = self.prefixe
|
||||
self._cache = {} # voyage → (empreinte des sources, analyse)
|
||||
self.routes = []
|
||||
for methode, motif, fn in [
|
||||
@@ -171,7 +175,7 @@ class Application:
|
||||
if not os.path.exists(chemin):
|
||||
modifier_toml(chemin, {"titre": "Nos Road-Trips"},
|
||||
entete="Réglages du site RoadTripsGenerator (rtg).")
|
||||
return Reponse("", entetes={"HX-Redirect": "/"})
|
||||
return Reponse("", entetes={"HX-Redirect": self.prefixe + "/"})
|
||||
|
||||
def groupes_existants(self):
|
||||
res = []
|
||||
@@ -197,7 +201,7 @@ class Application:
|
||||
return page("nouveau.html", **self.contexte(groupes=self.groupes_existants(),
|
||||
erreur=str(e), form=r.form))
|
||||
rel = os.path.relpath(chemin, self.racine).replace(os.sep, "/")
|
||||
return Reponse("", entetes={"HX-Redirect": f"/voyage?v={quote(rel)}#sources"})
|
||||
return Reponse("", entetes={"HX-Redirect": f"{self.prefixe}/voyage?v={quote(rel)}#sources"})
|
||||
|
||||
# --- Un voyage
|
||||
def voyage(self, r):
|
||||
@@ -359,11 +363,11 @@ class Application:
|
||||
nom = m["fichier"]
|
||||
if m["type"] == "photo":
|
||||
dossier = "photos_web" if os.path.exists(os.path.join(chemin, "photos_web", nom)) else "photos"
|
||||
vignette = f"/site/{quote(rel)}/{dossier}/{quote(nom)}"
|
||||
vignette = f"{self.prefixe}/site/{quote(rel)}/{dossier}/{quote(nom)}"
|
||||
else:
|
||||
base = os.path.splitext(nom)[0]
|
||||
ap = os.path.join(chemin, "apercus", base + ".jpg")
|
||||
vignette = f"/site/{quote(rel)}/apercus/{quote(base)}.jpg" if os.path.exists(ap) else None
|
||||
vignette = f"{self.prefixe}/site/{quote(rel)}/apercus/{quote(base)}.jpg" if os.path.exists(ap) else None
|
||||
d = m["date"][:10].replace(":", "-")
|
||||
return dict(m, jour=d, heure=m["date"][11:16], vignette=vignette)
|
||||
|
||||
@@ -501,6 +505,7 @@ class Application:
|
||||
"journees": {"fuseau": r.f("fuseau").strip() or "Europe/Paris",
|
||||
"coupure_heure": entier("coupure_heure", 4)},
|
||||
"carte_globale": {"mymaps": liens},
|
||||
"administration": {"adresse": r.f("administration").strip()},
|
||||
}, entete="Réglages du site RoadTripsGenerator (rtg).")
|
||||
return Reponse('<span class="ok">✓ Réglages enregistrés.</span>')
|
||||
|
||||
@@ -553,8 +558,10 @@ class Application:
|
||||
return page("_tache.html", tache=taches.COURANTE, message=message)
|
||||
|
||||
def tache_arreter(self, r):
|
||||
# La relève suivante (chaque seconde) affichera l'arrêt : remplacer le
|
||||
# bloc ici en même temps qu'elle ferait échouer l'une des deux.
|
||||
taches.arreter()
|
||||
return page("_tache.html", tache=taches.COURANTE)
|
||||
return Reponse("")
|
||||
|
||||
def tache(self, r):
|
||||
return page("tache.html", **self.contexte())
|
||||
@@ -619,8 +626,11 @@ PUBLIQUES = {"/connexion"}
|
||||
|
||||
|
||||
def creer_gestionnaire(app, port, gardien=None):
|
||||
gardien = gardien or Gardien(port)
|
||||
gardien = gardien or Gardien(port, prefixe=app.prefixe)
|
||||
hotes = {f"127.0.0.1:{port}", f"localhost:{port}"}
|
||||
P = app.prefixe
|
||||
# « Voir le site » : derrière le serveur web, le vrai site ; sinon le site servi par RTG.
|
||||
ENV.globals["lien_site"] = (P.rsplit("/", 1)[0] + "/") if gardien.proxy else P + "/site/"
|
||||
|
||||
class Gestionnaire(BaseHTTPRequestHandler):
|
||||
server_version = f"rtg/{__version__}"
|
||||
@@ -653,12 +663,26 @@ def creer_gestionnaire(app, port, gardien=None):
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def ip(self):
|
||||
"""Adresse du visiteur ; derrière le serveur web, celle qu'il transmet."""
|
||||
if gardien.proxy:
|
||||
transmise = (self.headers.get("X-Forwarded-For") or "").split(",")[-1].strip()
|
||||
if transmise:
|
||||
return transmise
|
||||
return self.client_address[0]
|
||||
|
||||
def traiter(self, methode, tete=False):
|
||||
try:
|
||||
if not gardien.distant and self.headers.get("Host") not in hotes:
|
||||
if not (gardien.distant or gardien.proxy) and self.headers.get("Host") not in hotes:
|
||||
raise Erreur(403, "Hôte refusé")
|
||||
url = urlsplit(self.path)
|
||||
chemin = unquote(url.path)
|
||||
if P:
|
||||
if chemin == P:
|
||||
return self.rediriger(P + "/")
|
||||
if not chemin.startswith(P + "/"):
|
||||
raise Erreur(404, "Page introuvable")
|
||||
chemin = chemin[len(P):]
|
||||
query = parse_qs(url.query, keep_blank_values=True)
|
||||
if methode == "GET" and chemin.startswith("/static/"):
|
||||
return self.fichier(STATIC, chemin[len("/static/"):], tete)
|
||||
@@ -667,8 +691,8 @@ def creer_gestionnaire(app, port, gardien=None):
|
||||
if "jeton" in query:
|
||||
if gardien.jeton_valide(_valeur(query, "jeton", "")):
|
||||
sid = gardien.ouvrir_session()
|
||||
return self.rediriger(url.path or "/", gardien.entete_cookie(sid))
|
||||
return self.rediriger("/connexion")
|
||||
return self.rediriger(P + chemin, gardien.entete_cookie(sid, self.https()))
|
||||
return self.rediriger(P + "/connexion")
|
||||
connecte = gardien.session_valide(self.session())
|
||||
form = {}
|
||||
if methode == "POST":
|
||||
@@ -682,12 +706,13 @@ def creer_gestionnaire(app, port, gardien=None):
|
||||
return self.envoyer(self.connexion(methode, form, connecte), tete)
|
||||
if not connecte:
|
||||
if self.headers.get("HX-Request") == "true":
|
||||
return self.envoyer(Reponse("", entetes={"HX-Redirect": "/connexion"}), tete)
|
||||
return self.rediriger("/connexion")
|
||||
return self.envoyer(Reponse("", entetes={"HX-Redirect": P + "/connexion"}), tete)
|
||||
return self.rediriger(P + "/connexion")
|
||||
if chemin == "/deconnexion" and methode == "POST":
|
||||
gardien.fermer_session(self.session())
|
||||
return self.envoyer(Reponse("", entetes={
|
||||
"HX-Redirect": "/connexion", "Set-Cookie": gardien.entete_cookie("", effacer=True)}), tete)
|
||||
"HX-Redirect": P + "/connexion",
|
||||
"Set-Cookie": gardien.entete_cookie("", self.https(), effacer=True)}), tete)
|
||||
if methode == "GET" and chemin.startswith("/site/"):
|
||||
return self.fichier(app.racine, chemin[len("/site/"):], tete)
|
||||
req = Requete(query, form)
|
||||
@@ -713,16 +738,25 @@ def creer_gestionnaire(app, port, gardien=None):
|
||||
rep.entetes = {"HX-Retarget": "#messages", "HX-Reswap": "innerHTML"}
|
||||
self.envoyer(rep, tete)
|
||||
|
||||
def https(self):
|
||||
return gardien.https or (gardien.proxy and self.headers.get("X-Forwarded-Proto") == "https")
|
||||
|
||||
def connexion(self, methode, form, connecte):
|
||||
ip = self.client_address[0]
|
||||
ip = self.ip()
|
||||
if methode == "POST":
|
||||
ok, message = gardien.essayer(ip, _valeur(form, "mot_de_passe", ""))
|
||||
if ok:
|
||||
sid = gardien.ouvrir_session()
|
||||
return Reponse("", entetes={"HX-Redirect": "/", "Set-Cookie": gardien.entete_cookie(sid)})
|
||||
return Reponse("", entetes={"HX-Redirect": P + "/",
|
||||
"Set-Cookie": gardien.entete_cookie(sid, self.https())})
|
||||
return Reponse(f'<div class="erreur">{_echapper(message)}</div>')
|
||||
return page("connexion.html", mdp=acces.defini(), connecte=connecte,
|
||||
distant=gardien.distant, attente=gardien.attente(ip))
|
||||
distant=gardien.distant or gardien.proxy, attente=gardien.attente(ip),
|
||||
site=self.adresse_site())
|
||||
|
||||
def adresse_site(self):
|
||||
"""Lien « Retour au site » de la page de connexion, derrière le serveur web."""
|
||||
return (P.rsplit("/", 1)[0] or "") + "/" if gardien.proxy else ""
|
||||
|
||||
def envoyer(self, rep, tete=False):
|
||||
self.send_response(rep.code)
|
||||
@@ -802,21 +836,26 @@ def _echapper(s):
|
||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=None):
|
||||
"""Lance l'interface. distant=True : écoute sur toutes les interfaces
|
||||
réseau, mot de passe obligatoire ; certificat et cle : HTTPS."""
|
||||
if distant and not acces.defini():
|
||||
raise SystemExit("❌ L'accès distant demande un mot de passe : lancez d'abord « rtg mot-de-passe ».")
|
||||
def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=None,
|
||||
proxy=False, prefixe=""):
|
||||
"""Lance l'interface.
|
||||
distant : écoute sur toutes les interfaces réseau, mot de passe obligatoire ;
|
||||
certificat, cle : HTTPS ;
|
||||
proxy : derrière le serveur web du site (qui transmet https://…/admin/ à
|
||||
127.0.0.1:port), mot de passe obligatoire ; prefixe : « /admin »."""
|
||||
if (distant or proxy) and not acces.defini():
|
||||
raise SystemExit("❌ L'accès depuis le réseau demande un mot de passe : lancez d'abord « rtg mot-de-passe ».")
|
||||
contexte_tls = None
|
||||
if certificat:
|
||||
contexte_tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
contexte_tls.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
contexte_tls.load_cert_chain(certificat, cle)
|
||||
app = Application(racine)
|
||||
app = Application(racine, prefixe)
|
||||
serveur = None
|
||||
for p in range(port, port + 20):
|
||||
# Derrière le serveur web, le port doit être exactement celui de sa configuration.
|
||||
for p in ([port] if proxy else range(port, port + 20)):
|
||||
try:
|
||||
gardien = Gardien(p, distant=distant, https=bool(contexte_tls))
|
||||
gardien = Gardien(p, distant=distant, https=bool(contexte_tls), proxy=proxy, prefixe=app.prefixe)
|
||||
serveur = ThreadingHTTPServer(("0.0.0.0" if distant else "127.0.0.1", p),
|
||||
creer_gestionnaire(app, p, gardien))
|
||||
port = p
|
||||
@@ -824,14 +863,16 @@ def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=N
|
||||
except OSError:
|
||||
continue
|
||||
if serveur is None:
|
||||
raise SystemExit(f"❌ Aucun port libre entre {port} et {port + 19}.")
|
||||
raise SystemExit(f"❌ Port {port} occupé." if proxy else f"❌ Aucun port libre entre {port} et {port + 19}.")
|
||||
if contexte_tls:
|
||||
serveur.socket = contexte_tls.wrap_socket(serveur.socket, server_side=True)
|
||||
serveur.daemon_threads = True
|
||||
schema = "https" if contexte_tls else "http"
|
||||
url = f"{schema}://127.0.0.1:{port}/?jeton={gardien.jeton}"
|
||||
url = f"{schema}://127.0.0.1:{port}{app.prefixe}/?jeton={gardien.jeton}"
|
||||
print(f"🌐 Interface RTG : {url}")
|
||||
print(" (cette adresse contient la clé d'accès de cette session : ne la partagez pas)")
|
||||
if proxy:
|
||||
print(f" Derrière le serveur web : https://<votre site>{app.prefixe}/ (mot de passe)")
|
||||
if distant:
|
||||
print(f" Accès depuis le réseau : {schema}://<nom ou adresse de cette machine>:{port}/ (mot de passe)")
|
||||
if not contexte_tls:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const racine = document.getElementById("editeur");
|
||||
if (!racine || !window.L) return;
|
||||
const voyage = racine.dataset.voyage;
|
||||
const base = document.body.dataset.base || ""; // « /admin » derrière le serveur web
|
||||
const $ = id => document.getElementById(id);
|
||||
let carte = null, donnees = null, coucheTraces, coucheMedias, coucheZones;
|
||||
let brouillon = null, modeZone = false, aPlacer = null, cadree = false;
|
||||
@@ -29,7 +30,7 @@
|
||||
}
|
||||
|
||||
function poster(url, params) {
|
||||
return fetch(url, {
|
||||
return fetch(base + url, {
|
||||
method: "POST",
|
||||
headers: { "HX-Request": "true", "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(Object.assign({ v: voyage }, params))
|
||||
@@ -41,7 +42,7 @@
|
||||
}
|
||||
|
||||
function charger() {
|
||||
return fetch("/voyage/carte.json?v=" + encodeURIComponent(voyage), { headers: { "HX-Request": "true" } })
|
||||
return fetch(base + "/voyage/carte.json?v=" + encodeURIComponent(voyage), { headers: { "HX-Request": "true" } })
|
||||
.then(lireReponse)
|
||||
.then(d => { donnees = d; remplirJours(); dessinerTout(); });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user