Administration Docker à la demande : profil admin, arrêt après 2 h d'inactivité, page « Administration fermée »
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
02473fafbf
commit
daaee62373
6 files changed
+49
-10
No files matched your search
+3
-2
@@ -16,9 +16,10 @@ RUN pip install --no-cache-dir . && rtg verifier
|
||||
# Le conteneur voit les disques du Mac sous /Volumes, aux mêmes chemins que
|
||||
# le Mac : les sources notées dans les voyage.toml restent valables.
|
||||
ENV XDG_CONFIG_HOME=/config TZ=Europe/Paris PYTHONUNBUFFERED=1 \
|
||||
RTG_RACINE=/Volumes/CLEROADTRIPS/Road-Trips RTG_PREFIXE=/admin
|
||||
RTG_RACINE=/Volumes/CLEROADTRIPS/Road-Trips RTG_PREFIXE=/admin \
|
||||
RTG_ARRET_INACTIF=120
|
||||
EXPOSE 8765
|
||||
|
||||
# Aucun port publié : seul le nginx du site (même réseau Docker) le joint.
|
||||
CMD exec rtg --racine "$RTG_RACINE" interface --proxy --prefixe "$RTG_PREFIXE" \
|
||||
--ecoute 0.0.0.0 --sans-navigateur
|
||||
--ecoute 0.0.0.0 --sans-navigateur --arret-inactif "$RTG_ARRET_INACTIF"
|
||||
@@ -183,14 +183,16 @@ Pour administrer le site depuis son adresse publique (bouton « Connexion » en
|
||||
|
||||
### L'administration dans Docker
|
||||
|
||||
Plutôt que de laisser `rtg interface` ouvert dans un Terminal, on peut le faire tourner dans un conteneur, à côté du nginx du site : il démarre avec Docker et redémarre tout seul. Le `Dockerfile` est à la racine de RoadTripsGenerator (Python, ffmpeg, exiftool).
|
||||
Plutôt que de laisser `rtg interface` ouvert dans un Terminal, on peut le faire tourner dans un conteneur, à côté du nginx du site. Le `Dockerfile` est à la racine de RoadTripsGenerator (Python, ffmpeg, exiftool).
|
||||
|
||||
1. Définir le mot de passe sur le Mac, une fois : `rtg mot-de-passe`. Le conteneur lit le même fichier.
|
||||
2. Ajouter le service `rtg` au `docker-compose.yml` du site : voir `exemples/docker-compose.yml`. Le conteneur voit `/Volumes` aux mêmes chemins que le Mac : la clé, le disque des rushs… Les sources notées dans les `voyage.toml` restent donc valables.
|
||||
3. Dans `nginx.conf`, faire pointer `/admin/` vers ce conteneur : voir `exemples/nginx-admin.conf`. Le site reste en ligne même si le conteneur RTG est arrêté.
|
||||
4. `docker compose up -d --build`.
|
||||
4. `docker compose build rtg`, puis `docker compose up -d` (le site ; l'administration ne démarre pas avec lui).
|
||||
|
||||
Après une mise à jour de RTG : `docker compose up -d --build rtg`. Un disque branché après le démarrage du conteneur peut demander `docker compose restart rtg`. L'encodage utilise les processeurs attribués à Docker (réglages de Docker Desktop, « Resources ») ; l'encodeur matériel VideoToolbox du Mac n'est pas disponible dans le conteneur.
|
||||
L'administration est **à la demande** : `docker compose up -d rtg` l'ouvre, `docker compose stop rtg` la ferme, et elle se ferme d'elle-même après 2 heures sans activité (variable `RTG_ARRET_INACTIF`, en minutes). Fermée, `/admin/` affiche « Administration fermée » et le site reste en ligne.
|
||||
|
||||
Après une mise à jour de RTG : `docker compose build rtg`. Un disque branché après le démarrage du conteneur peut demander `docker compose restart rtg`. L'encodage utilise les processeurs attribués à Docker (réglages de Docker Desktop, « Resources ») ; l'encodeur matériel VideoToolbox du Mac n'est pas disponible dans le conteneur.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# docker-compose.yml du site, avec l'administration RTG dans son propre conteneur.
|
||||
# Après une mise à jour de RTG : docker compose up -d --build rtg
|
||||
# Après une mise à jour de RTG : docker compose build rtg
|
||||
services:
|
||||
roadtrip:
|
||||
image: nginx:alpine
|
||||
@@ -15,9 +15,13 @@ services:
|
||||
- traefik.http.routers.ouaibe_roadtrip.rule=Host(`roadtrip.ouaibe.fr`) || Host(`roadtrips.ouaibe.fr`)
|
||||
- traefik.http.routers.ouaibe_roadtrip.entrypoints=websecure
|
||||
|
||||
# Administration à la demande : elle ne démarre pas avec le reste.
|
||||
# ouvrir : docker compose up -d rtg fermer : docker compose stop rtg
|
||||
# Elle se ferme d'elle-même après 2 h sans activité (RTG_ARRET_INACTIF, en minutes).
|
||||
rtg:
|
||||
build: /Volumes/CLEROADTRIPS/RoadTripsGenerator
|
||||
restart: unless-stopped
|
||||
profiles: ["admin"]
|
||||
restart: "no"
|
||||
networks:
|
||||
- ouaibe_network
|
||||
volumes:
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
# Conteneur RTG arrêté : une page claire plutôt qu'une erreur 502.
|
||||
error_page 502 504 = @admin_fermee;
|
||||
}
|
||||
|
||||
location @admin_fermee {
|
||||
default_type "text/html; charset=utf-8";
|
||||
add_header Cache-Control no-store always;
|
||||
return 503 "<!doctype html><html lang=fr><meta charset=utf-8><meta name=viewport content='width=device-width'><title>Administration fermée</title><body style='font-family:-apple-system,sans-serif;text-align:center;margin-top:18vh;color:#444'><h1>Administration fermée</h1><p>Elle n'est ouverte que pendant les mises à jour du site.</p><p><a href='/'>Retour au site</a></p></body></html>";
|
||||
}
|
||||
|
||||
# Réglages de RTG : inutile de les publier.
|
||||
|
||||
+3
-1
@@ -58,6 +58,8 @@ def main(argv=None):
|
||||
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")
|
||||
i.add_argument("--ecoute", help="adresse d'écoute (défaut 127.0.0.1 ; 0.0.0.0 si le serveur web est ailleurs)")
|
||||
i.add_argument("--arret-inactif", type=float, default=0, metavar="MINUTES",
|
||||
help="s'arrêter après MINUTES sans activité (0 = jamais)")
|
||||
|
||||
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")
|
||||
@@ -119,7 +121,7 @@ def main(argv=None):
|
||||
sys.exit("❌ --certificat et --cle vont ensemble.")
|
||||
lancer(racine, args.port, ouvrir=not (args.sans_navigateur or args.proxy), distant=args.distant,
|
||||
certificat=args.certificat, cle=args.cle, proxy=args.proxy, prefixe=args.prefixe,
|
||||
ecoute=args.ecoute)
|
||||
ecoute=args.ecoute, arret_inactif=args.arret_inactif)
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrompu.")
|
||||
sys.exit(130)
|
||||
@@ -26,6 +26,7 @@ import signal
|
||||
import ssl
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import date, timedelta
|
||||
@@ -107,6 +108,7 @@ class Application:
|
||||
self.prefixe = ("/" + prefixe.strip("/")) if prefixe.strip("/") else ""
|
||||
ENV.globals["base"] = self.prefixe
|
||||
self._cache = {} # voyage → (empreinte des sources, analyse)
|
||||
self.derniere_activite = time.time()
|
||||
self.routes = []
|
||||
for methode, motif, fn in [
|
||||
("GET", r"/", self.accueil),
|
||||
@@ -674,6 +676,10 @@ def creer_gestionnaire(app, port, gardien=None):
|
||||
return self.client_address[0]
|
||||
|
||||
def traiter(self, methode, tete=False):
|
||||
# Activité, pour l'arrêt automatique : les relèves périodiques d'un
|
||||
# onglet resté ouvert ne comptent pas.
|
||||
if not self.path.split("?")[0].endswith(("/tache/bandeau", "/tache/fragment")):
|
||||
app.derniere_activite = time.time()
|
||||
try:
|
||||
if not (gardien.distant or gardien.proxy) and self.headers.get("Host") not in hotes:
|
||||
raise Erreur(403, "Hôte refusé")
|
||||
@@ -861,14 +867,15 @@ def _echapper(s):
|
||||
|
||||
|
||||
def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=None,
|
||||
proxy=False, prefixe="", ecoute=None):
|
||||
proxy=False, prefixe="", ecoute=None, arret_inactif=0):
|
||||
"""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 » ;
|
||||
ecoute : adresse d'écoute imposée (« 0.0.0.0 » si le serveur web est sur
|
||||
une autre machine)."""
|
||||
une autre machine) ; arret_inactif : minutes sans activité (ni traitement en
|
||||
cours) au bout desquelles l'interface s'arrête d'elle-même, 0 = jamais."""
|
||||
hors_machine = ecoute and ecoute not in ("127.0.0.1", "localhost", "::1")
|
||||
if (distant or proxy or hors_machine) 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 ».")
|
||||
@@ -915,6 +922,21 @@ def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=N
|
||||
def sur_sigterm(*_):
|
||||
raise KeyboardInterrupt
|
||||
signal.signal(signal.SIGTERM, sur_sigterm)
|
||||
|
||||
if arret_inactif:
|
||||
print(f" Arrêt automatique après {arret_inactif:g} min sans activité.")
|
||||
|
||||
def surveiller():
|
||||
while True:
|
||||
time.sleep(min(30, arret_inactif * 30))
|
||||
t = taches.COURANTE
|
||||
if t and t.en_cours:
|
||||
app.derniere_activite = time.time()
|
||||
elif time.time() - app.derniere_activite > arret_inactif * 60:
|
||||
print(f"💤 Aucune activité depuis {arret_inactif:g} min : arrêt de l'interface.")
|
||||
serveur.shutdown()
|
||||
return
|
||||
threading.Thread(target=surveiller, daemon=True, name="rtg-veille").start()
|
||||
try:
|
||||
serveur.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in new issue
Block a user