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
@@ -143,8 +143,44 @@ L'interface permet de modifier le site et de lire les fichiers de l'ordinateur :
|
|||||||
- servir l'interface derrière le serveur web du site, en HTTPS, sur une adresse à part ;
|
- servir l'interface derrière le serveur web du site, en HTTPS, sur une adresse à part ;
|
||||||
- `--certificat cert.pem --cle cle.pem` ;
|
- `--certificat cert.pem --cle cle.pem` ;
|
||||||
- le plus simple : un tunnel SSH. Sur le serveur, lancer `rtg interface --sans-navigateur` (sans `--distant`). Sur le Mac, ouvrir `ssh -L 8765:127.0.0.1:8765 serveur`, puis l'adresse avec la clé affichée sur le serveur.
|
- le plus simple : un tunnel SSH. Sur le serveur, lancer `rtg interface --sans-navigateur` (sans `--distant`). Sur le Mac, ouvrir `ssh -L 8765:127.0.0.1:8765 serveur`, puis l'adresse avec la clé affichée sur le serveur.
|
||||||
|
- **Bouton « Connexion » sur l'accueil du site** : voir ci-dessous.
|
||||||
- Les fichiers de réglages (`site.toml`, `voyage.toml`) sont dans les dossiers du site. Si le serveur web publie ce dossier tel quel, ils sont lisibles par tous, avec les chemins de tes disques. Rien de secret n'y est écrit, mais on peut les refuser au serveur web (par exemple `location ~ \.toml$ { deny all; }` pour nginx, ou `<FilesMatch "\.toml$"> Require all denied </FilesMatch>` pour Apache).
|
- Les fichiers de réglages (`site.toml`, `voyage.toml`) sont dans les dossiers du site. Si le serveur web publie ce dossier tel quel, ils sont lisibles par tous, avec les chemins de tes disques. Rien de secret n'y est écrit, mais on peut les refuser au serveur web (par exemple `location ~ \.toml$ { deny all; }` pour nginx, ou `<FilesMatch "\.toml$"> Require all denied </FilesMatch>` pour Apache).
|
||||||
|
|
||||||
|
### Administration depuis le site
|
||||||
|
|
||||||
|
Pour administrer le site depuis son adresse publique (bouton « Connexion » en haut à droite de l'accueil, qui mène à `https://roadtrip.ouaibe.fr/admin/`), l'interface tourne sur la machine qui sert le site, derrière son serveur web en HTTPS. Les sources (rushs, traces, photos) doivent alors être accessibles depuis cette machine pour encoder et générer.
|
||||||
|
|
||||||
|
1. Sur cette machine, une fois : `rtg mot-de-passe`.
|
||||||
|
2. Lancer l'interface, en permanence de préférence (service, `tmux`…) :
|
||||||
|
```
|
||||||
|
rtg --racine /chemin/vers/Road-Trips interface --proxy --prefixe /admin --port 8765
|
||||||
|
```
|
||||||
|
Elle n'écoute que sur 127.0.0.1 : seul le serveur web peut la joindre. Le mot de passe est obligatoire.
|
||||||
|
3. Transmettre `/admin/` à l'interface depuis le serveur web.
|
||||||
|
- nginx, dans le bloc `server` du site en HTTPS :
|
||||||
|
```nginx
|
||||||
|
location /admin/ {
|
||||||
|
proxy_pass http://127.0.0.1:8765;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
location ~ \.toml$ { deny all; }
|
||||||
|
```
|
||||||
|
- Apache (modules `proxy`, `proxy_http`, `headers`) :
|
||||||
|
```apache
|
||||||
|
ProxyPass /admin/ http://127.0.0.1:8765/admin/
|
||||||
|
ProxyPassReverse /admin/ http://127.0.0.1:8765/admin/
|
||||||
|
RequestHeader set X-Forwarded-Proto "https"
|
||||||
|
<FilesMatch "\.toml$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
```
|
||||||
|
4. Dans l'interface, Réglages → « Administration depuis le site » : adresse `/admin/`. Après « Refaire l'accueil », le bouton « Connexion » apparaît sur la page d'accueil.
|
||||||
|
|
||||||
|
Le bouton n'est qu'un lien : la protection est celle de l'interface (mot de passe, blocage après plusieurs essais faux, cookie de session limité à `/admin/`, envoyé seulement en HTTPS).
|
||||||
|
|
||||||
Tout ce que fait l'interface s'écrit dans `site.toml` et les `voyage.toml`. On peut toujours utiliser les commandes ci-dessous ou modifier ces fichiers à la main. Une réécriture par l'interface conserve les valeurs mais pas les commentaires.
|
Tout ce que fait l'interface s'écrit dans `site.toml` et les `voyage.toml`. On peut toujours utiliser les commandes ci-dessous ou modifier ces fichiers à la main. Une réécriture par l'interface conserve les valeurs mais pas les commentaires.
|
||||||
|
|
||||||
## Utilisation : la commande `rtg`
|
## Utilisation : la commande `rtg`
|
||||||
@@ -160,6 +196,7 @@ rtg analyser Balades/Vexin-10-2026 # ce que RTG trouve dans les sources
|
|||||||
rtg verifier # tout est-il installé ?
|
rtg verifier # tout est-il installé ?
|
||||||
rtg interface # l'interface dans le navigateur
|
rtg interface # l'interface dans le navigateur
|
||||||
rtg mot-de-passe # mot de passe de l'interface (accès distant)
|
rtg mot-de-passe # mot de passe de l'interface (accès distant)
|
||||||
|
rtg interface --proxy --prefixe /admin # derrière le serveur web du site (bouton Connexion)
|
||||||
```
|
```
|
||||||
|
|
||||||
Les sources d'un voyage peuvent rester où elles sont, nommées n'importe comment : on les indique dans son `voyage.toml`.
|
Les sources d'un voyage peuvent rester où elles sont, nommées n'importe comment : on les indique dans son `voyage.toml`.
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ def generer(racine=".", cfg=None, mettre_a_jour_mymaps=False):
|
|||||||
|
|
||||||
ecrire("index.html", env.get_template("accueil.html").render(
|
ecrire("index.html", env.get_template("accueil.html").render(
|
||||||
titre_site=TITRE_SITE, rubriques=rubriques, nb_recents=NB_RECENTS,
|
titre_site=TITRE_SITE, rubriques=rubriques, nb_recents=NB_RECENTS,
|
||||||
|
administration=cfg["administration"]["adresse"],
|
||||||
carte_globale=CARTE_GLOBALE if any(r['titre'] == TITRE_SECTION_RACINE for r in rubriques) else None,
|
carte_globale=CARTE_GLOBALE if any(r['titre'] == TITRE_SECTION_RACINE for r in rubriques) else None,
|
||||||
sous_titre=f"{total} voyage{'s' if total > 1 else ''}" if total else ""))
|
sous_titre=f"{total} voyage{'s' if total > 1 else ''}" if total else ""))
|
||||||
for r in rubriques:
|
for r in rubriques:
|
||||||
|
|||||||
+5
-2
@@ -54,6 +54,9 @@ def main(argv=None):
|
|||||||
help="accepter les connexions depuis le réseau (mot de passe obligatoire)")
|
help="accepter les connexions depuis le réseau (mot de passe obligatoire)")
|
||||||
i.add_argument("--certificat", help="certificat TLS (PEM) pour servir en HTTPS")
|
i.add_argument("--certificat", help="certificat TLS (PEM) pour servir en HTTPS")
|
||||||
i.add_argument("--cle", help="clé privée du certificat TLS (PEM)")
|
i.add_argument("--cle", help="clé privée du certificat TLS (PEM)")
|
||||||
|
i.add_argument("--proxy", action="store_true",
|
||||||
|
help="derrière le serveur web du site (mot de passe obligatoire)")
|
||||||
|
i.add_argument("--prefixe", default="", help="chemin de l'interface sur le site, par exemple /admin")
|
||||||
|
|
||||||
mp = sp.add_parser("mot-de-passe", help="définir ou supprimer le mot de passe de l'interface")
|
mp = sp.add_parser("mot-de-passe", help="définir ou supprimer le mot de passe de l'interface")
|
||||||
mp.add_argument("--supprimer", action="store_true")
|
mp.add_argument("--supprimer", action="store_true")
|
||||||
@@ -105,8 +108,8 @@ def main(argv=None):
|
|||||||
from rtg.interface.serveur import lancer
|
from rtg.interface.serveur import lancer
|
||||||
if bool(args.certificat) != bool(args.cle):
|
if bool(args.certificat) != bool(args.cle):
|
||||||
sys.exit("❌ --certificat et --cle vont ensemble.")
|
sys.exit("❌ --certificat et --cle vont ensemble.")
|
||||||
lancer(racine, args.port, ouvrir=not args.sans_navigateur, distant=args.distant,
|
lancer(racine, args.port, ouvrir=not (args.sans_navigateur or args.proxy), distant=args.distant,
|
||||||
certificat=args.certificat, cle=args.cle)
|
certificat=args.certificat, cle=args.cle, proxy=args.proxy, prefixe=args.prefixe)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nInterrompu.")
|
print("\nInterrompu.")
|
||||||
sys.exit(130)
|
sys.exit(130)
|
||||||
@@ -32,6 +32,9 @@ DEFAUTS = {
|
|||||||
"photos": {"grand_cote": 1600, "qualite": 80, "positions_deduites": True},
|
"photos": {"grand_cote": 1600, "qualite": 80, "positions_deduites": True},
|
||||||
"journees": {"fuseau": "Europe/Paris", "coupure_heure": 4},
|
"journees": {"fuseau": "Europe/Paris", "coupure_heure": 4},
|
||||||
"carte_globale": {"mymaps": []},
|
"carte_globale": {"mymaps": []},
|
||||||
|
# Adresse de l'interface (rtg interface --proxy) : bouton « Connexion »
|
||||||
|
# sur l'accueil du site. Vide = pas de bouton.
|
||||||
|
"administration": {"adresse": ""},
|
||||||
# Propres à un voyage
|
# Propres à un voyage
|
||||||
"rubrique": "",
|
"rubrique": "",
|
||||||
"moto": "",
|
"moto": "",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><code>{{ nom }}</code> <span class="discret">{{ d.km|round|int }} km</span></td>
|
<td><code>{{ nom }}</code> <span class="discret">{{ d.km|round|int }} km</span></td>
|
||||||
<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="hidden" name="v" value="{{ v.chemin }}"><input type="hidden" name="fichier" value="{{ nom }}">
|
||||||
<input type="date" name="jour" required><button>OK</button>
|
<input type="date" name="jour" required><button>OK</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% set s = suggestions.get(nom) %}
|
{% set s = suggestions.get(nom) %}
|
||||||
{% if s and s[0] %}
|
{% 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 }}'
|
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>
|
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 %}
|
{% else %}<span class="discret">pas de suggestion</span>{% endif %}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if non_datees|length > 1 %}
|
{% 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 }}">
|
<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>
|
Ou bien, dans l'ordre des noms, un jour par trace à partir du <input type="date" name="debut" required>
|
||||||
<button>Répartir</button>
|
<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><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>{% for j in d.jours %}{{ j|date_courte }}{{ ", " if not loop.last }}{% else %}<span class="discret">—</span>{% endfor %}</td>
|
||||||
<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="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 '' }}">
|
<input type="date" name="jour" value="{{ d.impose.isoformat() if d.impose else '' }}">
|
||||||
<button class="petit">{{ "Changer" if d.impose else "Imposer" }}</button>
|
<button class="petit">{{ "Changer" if d.impose else "Imposer" }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% if d.impose %}
|
{% 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>
|
hx-vals='{{ {"v": v.chemin, "fichier": d.nom, "jour": ""}|tojson }}'>Annuler</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% if tache and tache.en_cours %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
@@ -3,14 +3,14 @@
|
|||||||
<h3>Choisir un dossier de {{ libelle }}</h3>
|
<h3>Choisir un dossier de {{ libelle }}</h3>
|
||||||
<div class="raccourcis">
|
<div class="raccourcis">
|
||||||
{% for nom, c in 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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<p class="chemin"><code>{{ chemin }}</code></p>
|
<p class="chemin"><code>{{ chemin }}</code></p>
|
||||||
<ul class="liste-dossiers">
|
<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 %}
|
{% 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 %}
|
{% else %}
|
||||||
<li class="vide">Pas de sous-dossier</li>
|
<li class="vide">Pas de sous-dossier</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<span class="{{ 'ok' if nb else 'discret' }}">{{ nb }} fichier{{ 's' if nb > 1 }} de {{ libelle }} ici</span>
|
<span class="{{ 'ok' if nb else 'discret' }}">{{ nb }} fichier{{ 's' if nb > 1 }} de {{ libelle }} ici</span>
|
||||||
<span>
|
<span>
|
||||||
<button data-fermer>Annuler</button>
|
<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>
|
hx-vals='{{ {"v": v, "cle": cle, "action": "ajouter", "chemin": chemin}|tojson }}'>Choisir ce dossier</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
<div><span class="etiquette">Encodage</span>
|
<div><span class="etiquette">Encodage</span>
|
||||||
{{ noms_qualites.get(cfg.video.qualite, cfg.video.qualite) }} · {{ "matériel" if i.encodage.encodeur == "vt" else "processeur" }}
|
{{ 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>
|
<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>
|
</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>
|
{% 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>
|
{% elif l.lien_casse %}<span class="pastille attention">disque débranché ?</span>
|
||||||
{% else %}<span class="pastille attention">introuvable</span>{% endif %}
|
{% 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>
|
hx-vals='{{ {"v": v.chemin, "cle": cle, "action": "retirer", "chemin": l.source}|tojson }}'>Retirer</button>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="ajout">
|
<div class="ajout">
|
||||||
<button hx-get="/dossiers?cle={{ cle }}&v={{ v.chemin|url }}" hx-target="#fenetre">Choisir un dossier…</button>
|
<button hx-get="{{ base }}/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">
|
<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="v" value="{{ v.chemin }}">
|
||||||
<input type="hidden" name="cle" value="{{ cle }}">
|
<input type="hidden" name="cle" value="{{ cle }}">
|
||||||
<input type="hidden" name="action" value="ajouter">
|
<input type="hidden" name="action" value="ajouter">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{% if message %}<div class="erreur">{{ message }}</div>{% endif %}
|
{% if message %}<div class="erreur">{{ message }}</div>{% endif %}
|
||||||
{% if tache %}
|
{% if tache %}
|
||||||
<div class="tache {{ 'en-cours' if tache.en_cours else ('ok' if tache.ok else 'echec') }}"
|
<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">
|
<div class="tache-tete">
|
||||||
<strong>{{ tache.libelle }}</strong>
|
<strong>{{ tache.libelle }}</strong>
|
||||||
<span class="etape">{{ "⏳ " if tache.en_cours }}{{ tache.etape }}{% if tache.etape_voyage %} — {{ tache.etape_voyage }}{% endif %}</span>
|
<span class="etape">{{ "⏳ " if tache.en_cours }}{{ tache.etape }}{% if tache.etape_voyage %} — {{ tache.etape_voyage }}{% endif %}</span>
|
||||||
<span class="discret">{{ tache.duree }}</span>
|
<span class="discret">{{ tache.duree }}</span>
|
||||||
{% if tache.en_cours %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if tache.video and tache.video.rang %}
|
{% if tache.video and tache.video.rang %}
|
||||||
|
|||||||
@@ -4,21 +4,21 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block titre %}{% endblock %} · RTG</title>
|
<title>{% block titre %}{% endblock %} · RTG</title>
|
||||||
<link rel="stylesheet" href="/static/rtg.css">
|
<link rel="stylesheet" href="{{ base }}/static/rtg.css">
|
||||||
{% block tete %}{% endblock %}
|
{% block tete %}{% endblock %}
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="{{ base }}/static/htmx.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-base="{{ base }}">
|
||||||
<header class="barre">
|
<header class="barre">
|
||||||
<a class="logo" href="/">RTG</a>
|
<a class="logo" href="{{ base }}/">RTG</a>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Mes voyages</a>
|
<a href="{{ base }}/">Mes voyages</a>
|
||||||
<a href="/reglages">Réglages</a>
|
<a href="{{ base }}/reglages">Réglages</a>
|
||||||
<a href="/tache">Traitement</a>
|
<a href="{{ base }}/tache">Traitement</a>
|
||||||
<a href="/site/" target="_blank">Voir le site ↗</a>
|
<a href="{{ lien_site }}" target="_blank">Voir le site ↗</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="bandeau" hx-get="/tache/bandeau" hx-trigger="load, every 2s"></div>
|
<div id="bandeau" hx-get="{{ base }}/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>
|
<button class="petit discret-bouton" hx-post="{{ base }}/deconnexion" title="Fermer la session de ce navigateur">Se déconnecter</button>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{% block contenu %}{% endblock %}
|
{% block contenu %}{% endblock %}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div id="messages" aria-live="polite"></div>
|
<div id="messages" aria-live="polite"></div>
|
||||||
<div id="fenetre"></div>
|
<div id="fenetre"></div>
|
||||||
<footer>RoadTripsGenerator {{ version }} · <code>{{ racine }}</code></footer>
|
<footer>RoadTripsGenerator {{ version }} · <code>{{ racine }}</code></footer>
|
||||||
<script src="/static/rtg.js"></script>
|
<script src="{{ base }}/static/rtg.js"></script>
|
||||||
{% block fin %}{% endblock %}
|
{% block fin %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -4,15 +4,15 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Connexion · RTG</title>
|
<title>Connexion · RTG</title>
|
||||||
<link rel="stylesheet" href="/static/rtg.css">
|
<link rel="stylesheet" href="{{ base }}/static/rtg.css">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="{{ base }}/static/htmx.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="page-connexion">
|
<body class="page-connexion">
|
||||||
<main class="connexion">
|
<main class="connexion">
|
||||||
<p class="logo">RTG</p>
|
<p class="logo">RTG</p>
|
||||||
{% if connecte %}
|
{% if connecte %}
|
||||||
<p>Vous êtes connecté.</p>
|
<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 %}
|
{% else %}
|
||||||
<h1>Accès à l'interface</h1>
|
<h1>Accès à l'interface</h1>
|
||||||
{% if not distant %}
|
{% 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>
|
elle contient la clé d'accès de cette session{% if mdp %}, ou entrez le mot de passe{% endif %}.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mdp %}
|
{% if mdp %}
|
||||||
<form hx-post="/connexion" hx-target="#retour-connexion">
|
<form hx-post="{{ base }}/connexion" hx-target="#retour-connexion">
|
||||||
<label>Mot de passe
|
<label>Mot de passe
|
||||||
<input type="password" name="mot_de_passe" autocomplete="current-password" required autofocus>
|
<input type="password" name="mot_de_passe" autocomplete="current-password" required autofocus>
|
||||||
</label>
|
</label>
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
<code>rtg mot-de-passe</code>.</p>
|
<code>rtg mot-de-passe</code>.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if site %}<p class="aide"><a href="{{ site }}">← Retour au site</a></p>{% endif %}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block titre %}Nouveau voyage{% endblock %}
|
{% block titre %}Nouveau voyage{% endblock %}
|
||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<h1>Nouveau voyage</h1>
|
<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 %}
|
{% if erreur %}<div class="erreur">{{ erreur }}</div>{% endif %}
|
||||||
<label>Titre
|
<label>Titre
|
||||||
<input name="titre" value="{{ form.titre[0] if form and form.titre else '' }}" placeholder="Été 2027 en Écosse" autofocus>
|
<input name="titre" value="{{ form.titre[0] if form and form.titre else '' }}" placeholder="Été 2027 en Écosse" autofocus>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<label class="choix">Nouvelle rubrique : <input name="groupe_nouveau" placeholder="Balades"></label>
|
<label class="choix">Nouvelle rubrique : <input name="groupe_nouveau" placeholder="Balades"></label>
|
||||||
</fieldset>
|
</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>
|
<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>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<h1>Réglages du site</h1>
|
<h1>Réglages du site</h1>
|
||||||
<p class="aide">Enregistrés dans <code>site.toml</code>. Ils valent pour tous les voyages.</p>
|
<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>
|
<fieldset>
|
||||||
<legend>Site</legend>
|
<legend>Site</legend>
|
||||||
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
<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>
|
<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>
|
<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>
|
||||||
|
<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>
|
<p><button class="principal">Enregistrer</button> <span id="retour-reglages"></span></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Système</h2>
|
<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 %}
|
{% endblock %}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<h1>Traitement</h1>
|
<h1>Traitement</h1>
|
||||||
{% if tache %}
|
{% 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 %}
|
{% with complet = True %}{% include "_tache.html" %}{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="vide">Aucun traitement lancé depuis l'ouverture de l'interface.</p>
|
<p class="vide">Aucun traitement lancé depuis l'ouverture de l'interface.</p>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block titre %}{{ v.titre }}{% endblock %}
|
{% block titre %}{{ v.titre }}{% endblock %}
|
||||||
{% block tete %}<link rel="stylesheet" href="/static/leaflet.css"><script src="/static/leaflet.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="/static/editeur.js"></script>{% endblock %}
|
{% block fin %}<script src="{{ base }}/static/editeur.js"></script>{% endblock %}
|
||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<div class="entete-page">
|
<div class="entete-page">
|
||||||
<div>
|
<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>
|
<h1>{{ v.titre }}</h1>
|
||||||
<p class="discret">{{ [periode(v.debut, v.fin), v.moto]|select|join(" · ") }} <code>{{ v.chemin }}</code></p>
|
<p class="discret">{{ [periode(v.debut, v.fin), v.moto]|select|join(" · ") }} <code>{{ v.chemin }}</code></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-journees">
|
<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>
|
<p class="chargement">Lecture des traces…</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -71,14 +71,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-infos">
|
<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 }}">
|
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||||
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
<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>
|
<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 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>
|
<p><button class="principal">Enregistrer</button> <span class="retour"></span></p>
|
||||||
</form>
|
</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 }}">
|
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||||
<label>Récit (<code>voyage.md</code>, en Markdown)
|
<label>Récit (<code>voyage.md</code>, en Markdown)
|
||||||
<textarea name="recit" rows="18" spellcheck="true">{{ recit }}</textarea>
|
<textarea name="recit" rows="18" spellcheck="true">{{ recit }}</textarea>
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-generer">
|
<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>
|
<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="/tache/lancer" hx-target="#tache-zone">
|
<form class="formulaire" hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
||||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
<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="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>
|
<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 %}
|
{% if not site_existe %}
|
||||||
<div class="encart">
|
<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><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>
|
<p class="aide">Pour un autre dossier, relancez avec <code>rtg interface --racine CHEMIN</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
<div class="entete-page">
|
<div class="entete-page">
|
||||||
<h1>Mes voyages <span class="compte">{{ nb }}</span></h1>
|
<h1>Mes voyages <span class="compte">{{ nb }}</span></h1>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="bouton principal" href="/nouveau">+ Nouveau voyage</a>
|
<a class="bouton principal" href="{{ base }}/nouveau">+ Nouveau voyage</a>
|
||||||
<form hx-post="/tache/lancer" hx-target="#tache-zone">
|
<form hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
||||||
<input type="hidden" name="quoi" value="accueil">
|
<input type="hidden" name="quoi" value="accueil">
|
||||||
<button title="index.html, pages des rubriques et carte globale">Refaire l'accueil</button>
|
<button title="index.html, pages des rubriques et carte globale">Refaire l'accueil</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for v in liste %}
|
{% for v in liste %}
|
||||||
<tr>
|
<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>
|
{% if v.moto %}<div class="discret">{{ v.moto }}</div>{% endif %}</td>
|
||||||
<td>{{ periode(v.debut, v.fin) }}</td>
|
<td>{{ periode(v.debut, v.fin) }}</td>
|
||||||
<td class="num">{{ v.jours or "—" }}</td>
|
<td class="num">{{ v.jours or "—" }}</td>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
{% else %}<span class="pastille attention">à refaire</span>{% endif %}
|
{% 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 %}
|
{% if v.a_verifier %}<span class="pastille attention" title="{{ v.a_verifier|join('\n') }}">⚠️ {{ v.a_verifier|length }}</span>{% endif %}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ ESSAIS_LIBRES = 5
|
|||||||
|
|
||||||
|
|
||||||
class Gardien:
|
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.jeton = secrets.token_urlsafe(24)
|
||||||
self.cookie = f"rtg_{port}"
|
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._sessions = {}
|
||||||
self._echecs = {} # ip → (nombre d'échecs, heure du dernier)
|
self._echecs = {} # ip → (nombre d'échecs, heure du dernier)
|
||||||
self._verrou = threading.Lock()
|
self._verrou = threading.Lock()
|
||||||
@@ -55,11 +56,11 @@ class Gardien:
|
|||||||
with self._verrou:
|
with self._verrou:
|
||||||
self._sessions.pop(sid, None)
|
self._sessions.pop(sid, None)
|
||||||
|
|
||||||
def entete_cookie(self, sid, effacer=False):
|
def entete_cookie(self, sid, https=None, effacer=False):
|
||||||
valeur = f"{self.cookie}={'' if effacer else sid}; Path=/; HttpOnly; SameSite=Strict"
|
valeur = f"{self.cookie}={'' if effacer else sid}; Path={self.chemin_cookie}; HttpOnly; SameSite=Strict"
|
||||||
if effacer:
|
if effacer:
|
||||||
valeur += "; Max-Age=0"
|
valeur += "; Max-Age=0"
|
||||||
if self.https:
|
if self.https if https is None else https:
|
||||||
valeur += "; Secure"
|
valeur += "; Secure"
|
||||||
return valeur
|
return valeur
|
||||||
|
|
||||||
|
|||||||
+67
-26
@@ -99,8 +99,12 @@ def json_rep(donnees):
|
|||||||
|
|
||||||
# --- Application ---------------------------------------------------------------
|
# --- Application ---------------------------------------------------------------
|
||||||
class Application:
|
class Application:
|
||||||
def __init__(self, racine):
|
def __init__(self, racine, prefixe=""):
|
||||||
self.racine = os.path.abspath(racine)
|
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._cache = {} # voyage → (empreinte des sources, analyse)
|
||||||
self.routes = []
|
self.routes = []
|
||||||
for methode, motif, fn in [
|
for methode, motif, fn in [
|
||||||
@@ -171,7 +175,7 @@ class Application:
|
|||||||
if not os.path.exists(chemin):
|
if not os.path.exists(chemin):
|
||||||
modifier_toml(chemin, {"titre": "Nos Road-Trips"},
|
modifier_toml(chemin, {"titre": "Nos Road-Trips"},
|
||||||
entete="Réglages du site RoadTripsGenerator (rtg).")
|
entete="Réglages du site RoadTripsGenerator (rtg).")
|
||||||
return Reponse("", entetes={"HX-Redirect": "/"})
|
return Reponse("", entetes={"HX-Redirect": self.prefixe + "/"})
|
||||||
|
|
||||||
def groupes_existants(self):
|
def groupes_existants(self):
|
||||||
res = []
|
res = []
|
||||||
@@ -197,7 +201,7 @@ class Application:
|
|||||||
return page("nouveau.html", **self.contexte(groupes=self.groupes_existants(),
|
return page("nouveau.html", **self.contexte(groupes=self.groupes_existants(),
|
||||||
erreur=str(e), form=r.form))
|
erreur=str(e), form=r.form))
|
||||||
rel = os.path.relpath(chemin, self.racine).replace(os.sep, "/")
|
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
|
# --- Un voyage
|
||||||
def voyage(self, r):
|
def voyage(self, r):
|
||||||
@@ -359,11 +363,11 @@ class Application:
|
|||||||
nom = m["fichier"]
|
nom = m["fichier"]
|
||||||
if m["type"] == "photo":
|
if m["type"] == "photo":
|
||||||
dossier = "photos_web" if os.path.exists(os.path.join(chemin, "photos_web", nom)) else "photos"
|
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:
|
else:
|
||||||
base = os.path.splitext(nom)[0]
|
base = os.path.splitext(nom)[0]
|
||||||
ap = os.path.join(chemin, "apercus", base + ".jpg")
|
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(":", "-")
|
d = m["date"][:10].replace(":", "-")
|
||||||
return dict(m, jour=d, heure=m["date"][11:16], vignette=vignette)
|
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",
|
"journees": {"fuseau": r.f("fuseau").strip() or "Europe/Paris",
|
||||||
"coupure_heure": entier("coupure_heure", 4)},
|
"coupure_heure": entier("coupure_heure", 4)},
|
||||||
"carte_globale": {"mymaps": liens},
|
"carte_globale": {"mymaps": liens},
|
||||||
|
"administration": {"adresse": r.f("administration").strip()},
|
||||||
}, entete="Réglages du site RoadTripsGenerator (rtg).")
|
}, entete="Réglages du site RoadTripsGenerator (rtg).")
|
||||||
return Reponse('<span class="ok">✓ Réglages enregistrés.</span>')
|
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)
|
return page("_tache.html", tache=taches.COURANTE, message=message)
|
||||||
|
|
||||||
def tache_arreter(self, r):
|
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()
|
taches.arreter()
|
||||||
return page("_tache.html", tache=taches.COURANTE)
|
return Reponse("")
|
||||||
|
|
||||||
def tache(self, r):
|
def tache(self, r):
|
||||||
return page("tache.html", **self.contexte())
|
return page("tache.html", **self.contexte())
|
||||||
@@ -619,8 +626,11 @@ PUBLIQUES = {"/connexion"}
|
|||||||
|
|
||||||
|
|
||||||
def creer_gestionnaire(app, port, gardien=None):
|
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}"}
|
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):
|
class Gestionnaire(BaseHTTPRequestHandler):
|
||||||
server_version = f"rtg/{__version__}"
|
server_version = f"rtg/{__version__}"
|
||||||
@@ -653,12 +663,26 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
self.send_header("Content-Length", "0")
|
self.send_header("Content-Length", "0")
|
||||||
self.end_headers()
|
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):
|
def traiter(self, methode, tete=False):
|
||||||
try:
|
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é")
|
raise Erreur(403, "Hôte refusé")
|
||||||
url = urlsplit(self.path)
|
url = urlsplit(self.path)
|
||||||
chemin = unquote(url.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)
|
query = parse_qs(url.query, keep_blank_values=True)
|
||||||
if methode == "GET" and chemin.startswith("/static/"):
|
if methode == "GET" and chemin.startswith("/static/"):
|
||||||
return self.fichier(STATIC, chemin[len("/static/"):], tete)
|
return self.fichier(STATIC, chemin[len("/static/"):], tete)
|
||||||
@@ -667,8 +691,8 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
if "jeton" in query:
|
if "jeton" in query:
|
||||||
if gardien.jeton_valide(_valeur(query, "jeton", "")):
|
if gardien.jeton_valide(_valeur(query, "jeton", "")):
|
||||||
sid = gardien.ouvrir_session()
|
sid = gardien.ouvrir_session()
|
||||||
return self.rediriger(url.path or "/", gardien.entete_cookie(sid))
|
return self.rediriger(P + chemin, gardien.entete_cookie(sid, self.https()))
|
||||||
return self.rediriger("/connexion")
|
return self.rediriger(P + "/connexion")
|
||||||
connecte = gardien.session_valide(self.session())
|
connecte = gardien.session_valide(self.session())
|
||||||
form = {}
|
form = {}
|
||||||
if methode == "POST":
|
if methode == "POST":
|
||||||
@@ -682,12 +706,13 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
return self.envoyer(self.connexion(methode, form, connecte), tete)
|
return self.envoyer(self.connexion(methode, form, connecte), tete)
|
||||||
if not connecte:
|
if not connecte:
|
||||||
if self.headers.get("HX-Request") == "true":
|
if self.headers.get("HX-Request") == "true":
|
||||||
return self.envoyer(Reponse("", entetes={"HX-Redirect": "/connexion"}), tete)
|
return self.envoyer(Reponse("", entetes={"HX-Redirect": P + "/connexion"}), tete)
|
||||||
return self.rediriger("/connexion")
|
return self.rediriger(P + "/connexion")
|
||||||
if chemin == "/deconnexion" and methode == "POST":
|
if chemin == "/deconnexion" and methode == "POST":
|
||||||
gardien.fermer_session(self.session())
|
gardien.fermer_session(self.session())
|
||||||
return self.envoyer(Reponse("", entetes={
|
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/"):
|
if methode == "GET" and chemin.startswith("/site/"):
|
||||||
return self.fichier(app.racine, chemin[len("/site/"):], tete)
|
return self.fichier(app.racine, chemin[len("/site/"):], tete)
|
||||||
req = Requete(query, form)
|
req = Requete(query, form)
|
||||||
@@ -713,16 +738,25 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
rep.entetes = {"HX-Retarget": "#messages", "HX-Reswap": "innerHTML"}
|
rep.entetes = {"HX-Retarget": "#messages", "HX-Reswap": "innerHTML"}
|
||||||
self.envoyer(rep, tete)
|
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):
|
def connexion(self, methode, form, connecte):
|
||||||
ip = self.client_address[0]
|
ip = self.ip()
|
||||||
if methode == "POST":
|
if methode == "POST":
|
||||||
ok, message = gardien.essayer(ip, _valeur(form, "mot_de_passe", ""))
|
ok, message = gardien.essayer(ip, _valeur(form, "mot_de_passe", ""))
|
||||||
if ok:
|
if ok:
|
||||||
sid = gardien.ouvrir_session()
|
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 Reponse(f'<div class="erreur">{_echapper(message)}</div>')
|
||||||
return page("connexion.html", mdp=acces.defini(), connecte=connecte,
|
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):
|
def envoyer(self, rep, tete=False):
|
||||||
self.send_response(rep.code)
|
self.send_response(rep.code)
|
||||||
@@ -802,21 +836,26 @@ def _echapper(s):
|
|||||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
|
||||||
def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=None):
|
def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=None,
|
||||||
"""Lance l'interface. distant=True : écoute sur toutes les interfaces
|
proxy=False, prefixe=""):
|
||||||
réseau, mot de passe obligatoire ; certificat et cle : HTTPS."""
|
"""Lance l'interface.
|
||||||
if distant and not acces.defini():
|
distant : écoute sur toutes les interfaces réseau, mot de passe obligatoire ;
|
||||||
raise SystemExit("❌ L'accès distant demande un mot de passe : lancez d'abord « rtg mot-de-passe ».")
|
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
|
contexte_tls = None
|
||||||
if certificat:
|
if certificat:
|
||||||
contexte_tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
contexte_tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
contexte_tls.minimum_version = ssl.TLSVersion.TLSv1_2
|
contexte_tls.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
contexte_tls.load_cert_chain(certificat, cle)
|
contexte_tls.load_cert_chain(certificat, cle)
|
||||||
app = Application(racine)
|
app = Application(racine, prefixe)
|
||||||
serveur = None
|
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:
|
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),
|
serveur = ThreadingHTTPServer(("0.0.0.0" if distant else "127.0.0.1", p),
|
||||||
creer_gestionnaire(app, p, gardien))
|
creer_gestionnaire(app, p, gardien))
|
||||||
port = p
|
port = p
|
||||||
@@ -824,14 +863,16 @@ def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=N
|
|||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
if serveur is None:
|
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:
|
if contexte_tls:
|
||||||
serveur.socket = contexte_tls.wrap_socket(serveur.socket, server_side=True)
|
serveur.socket = contexte_tls.wrap_socket(serveur.socket, server_side=True)
|
||||||
serveur.daemon_threads = True
|
serveur.daemon_threads = True
|
||||||
schema = "https" if contexte_tls else "http"
|
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(f"🌐 Interface RTG : {url}")
|
||||||
print(" (cette adresse contient la clé d'accès de cette session : ne la partagez pas)")
|
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:
|
if distant:
|
||||||
print(f" Accès depuis le réseau : {schema}://<nom ou adresse de cette machine>:{port}/ (mot de passe)")
|
print(f" Accès depuis le réseau : {schema}://<nom ou adresse de cette machine>:{port}/ (mot de passe)")
|
||||||
if not contexte_tls:
|
if not contexte_tls:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
const racine = document.getElementById("editeur");
|
const racine = document.getElementById("editeur");
|
||||||
if (!racine || !window.L) return;
|
if (!racine || !window.L) return;
|
||||||
const voyage = racine.dataset.voyage;
|
const voyage = racine.dataset.voyage;
|
||||||
|
const base = document.body.dataset.base || ""; // « /admin » derrière le serveur web
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
let carte = null, donnees = null, coucheTraces, coucheMedias, coucheZones;
|
let carte = null, donnees = null, coucheTraces, coucheMedias, coucheZones;
|
||||||
let brouillon = null, modeZone = false, aPlacer = null, cadree = false;
|
let brouillon = null, modeZone = false, aPlacer = null, cadree = false;
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function poster(url, params) {
|
function poster(url, params) {
|
||||||
return fetch(url, {
|
return fetch(base + url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "HX-Request": "true", "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "HX-Request": "true", "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: new URLSearchParams(Object.assign({ v: voyage }, params))
|
body: new URLSearchParams(Object.assign({ v: voyage }, params))
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function charger() {
|
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(lireReponse)
|
||||||
.then(d => { donnees = d; remplirJours(); dessinerTout(); });
|
.then(d => { donnees = d; remplirJours(); dessinerTout(); });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@
|
|||||||
display: block; width: 100%; height: min(600px, 75vh); border: none;
|
display: block; width: 100%; height: min(600px, 75vh); border: none;
|
||||||
border-radius: 15px; box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
border-radius: 15px; box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
/* Bouton discret vers l'interface d'administration (site.toml, [administration]). */
|
||||||
|
a.connexion { position: absolute; top: 16px; right: 20px; font-size: 0.85rem; color: #777;
|
||||||
|
text-decoration: none; padding: 5px 12px; border: 1px solid #ccc; border-radius: 99px; }
|
||||||
|
a.connexion:hover { color: #3498db; border-color: #3498db; }
|
||||||
.vide { text-align: center; color: #777; margin-top: 60px; line-height: 1.7; }
|
.vide { text-align: center; color: #777; margin-top: 60px; line-height: 1.7; }
|
||||||
code { background: #e8e8ef; padding: 2px 6px; border-radius: 4px; }
|
code { background: #e8e8ef; padding: 2px 6px; border-radius: 4px; }
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -62,10 +66,12 @@
|
|||||||
.sous-titre, .vide, .periode { color: #9a9aa2; }
|
.sous-titre, .vide, .periode { color: #9a9aa2; }
|
||||||
.rubrique-titre { border-color: #333; }
|
.rubrique-titre { border-color: #333; }
|
||||||
code { background: #2e2e36; }
|
code { background: #2e2e36; }
|
||||||
|
a.connexion { color: #9a9aa2; border-color: #444; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if administration %}<a class="connexion" href="{{ administration }}" rel="nofollow">Connexion</a>{% endif %}
|
||||||
<header>
|
<header>
|
||||||
<h1>{% block entete %}🌍 {{ titre_site }}{% endblock %}</h1>
|
<h1>{% block entete %}🌍 {{ titre_site }}{% endblock %}</h1>
|
||||||
<div class="sous-titre">{% block sous_titre %}{% endblock %}</div>
|
<div class="sous-titre">{% block sous_titre %}{% endblock %}</div>
|
||||||
|
|||||||
@@ -176,3 +176,46 @@ def test_corrections(client):
|
|||||||
for pistes in v.traces.values():
|
for pistes in v.traces.values():
|
||||||
for piste, pts in pistes:
|
for piste, pts in pistes:
|
||||||
assert all(corrections.distance_m(la, lo, 56.7, 2.24) > 3000 for la, lo in pts)
|
assert all(corrections.distance_m(la, lo, 56.7, 2.24) > 3000 for la, lo in pts)
|
||||||
|
|
||||||
|
|
||||||
|
def test_derriere_le_serveur_web(voyage_sans_heures, tmp_path, monkeypatch):
|
||||||
|
"""--proxy --prefixe /admin : adresses préfixées, mot de passe, cookie Secure."""
|
||||||
|
racine, voyage = voyage_sans_heures
|
||||||
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
|
||||||
|
monkeypatch.setenv("APPDATA", str(tmp_path / "config"))
|
||||||
|
acces.definir("un mot de passe solide")
|
||||||
|
app = serveur.Application(str(racine), "/admin")
|
||||||
|
srv = ThreadingHTTPServer(("127.0.0.1", 0), None)
|
||||||
|
port = srv.server_address[1]
|
||||||
|
gardien = Gardien(port, proxy=True, prefixe=app.prefixe)
|
||||||
|
srv.RequestHandlerClass = serveur.creer_gestionnaire(app, port, gardien)
|
||||||
|
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||||
|
ouvreur = urllib.request.build_opener(_SansRedirection)
|
||||||
|
entetes = {"Host": "roadtrip.example", "X-Forwarded-For": "203.0.113.7", "X-Forwarded-Proto": "https"}
|
||||||
|
|
||||||
|
def requete(chemin, donnees=None, extra=None):
|
||||||
|
req = urllib.request.Request(f"http://127.0.0.1:{port}{chemin}", headers=dict(entetes, **(extra or {})),
|
||||||
|
data=urllib.parse.urlencode(donnees).encode() if donnees else None)
|
||||||
|
try:
|
||||||
|
return ouvreur.open(req)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e
|
||||||
|
try:
|
||||||
|
r = requete("/admin/")
|
||||||
|
assert r.status == 303 and r.headers["Location"] == "/admin/connexion"
|
||||||
|
assert requete("/").status == 404 # hors du préfixe
|
||||||
|
assert requete("/admin/connexion").status == 200
|
||||||
|
r = requete("/admin/connexion", {"mot_de_passe": "un mot de passe solide"}, {"HX-Request": "true"})
|
||||||
|
cookie = r.headers["Set-Cookie"]
|
||||||
|
assert r.headers["HX-Redirect"] == "/admin/" and "Path=/admin/" in cookie and "Secure" in cookie
|
||||||
|
sid = cookie.split(";")[0]
|
||||||
|
r = requete("/admin/", extra={"Cookie": sid})
|
||||||
|
html = r.read().decode()
|
||||||
|
assert r.status == 200 and 'href="/admin/nouveau"' in html and 'href="/"' in html # Voir le site
|
||||||
|
# Les échecs sont comptés par visiteur (X-Forwarded-For), pas pour le serveur web
|
||||||
|
for _ in range(5):
|
||||||
|
requete("/admin/connexion", {"mot_de_passe": "faux"}, {"HX-Request": "true"})
|
||||||
|
assert gardien.attente("203.0.113.7") > 0 and gardien.attente("127.0.0.1") == 0
|
||||||
|
finally:
|
||||||
|
srv.shutdown()
|
||||||
|
serveur.ENV.globals["base"] = ""
|
||||||
Reference in new issue
Block a user