Interface : corrections à la souris et accès protégé

Onglet « Carte et corrections » : glisser une photo ou une vidéo, placer
les médias sans position, masquer des zones de trace (pour un voyage, ou
zone privée pour tous). Corrections notées dans voyage.toml ([positions],
[corrections]) ou site.toml ([confidentialite]), appliquées par les pages ;
les originaux ne changent pas. L'extraction garde les médias datés sans
position pour qu'on puisse les placer.

Accès : clé d'accès aléatoire à chaque lancement, session par cookie
HttpOnly/SameSite=Strict, mot de passe facultatif (rtg mot-de-passe,
empreinte scrypt hors du site, blocage après 5 échecs), accès réseau
seulement avec mot de passe (--distant), HTTPS possible (--certificat,
--cle), Content-Security-Policy et interdiction des cadres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
FabriceandClaude Opus 5.5 committed 2026-10-09 14:35:43 +00:00
1 parent 06153a227e
commit 71779f6f53
17 files changed
+1047 -38

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
// Onglet « Carte et corrections » : déplacer ou placer les photos et vidéos,
// masquer des zones de trace. Chaque correction est enregistrée aussitôt dans
// voyage.toml (ou site.toml pour une zone privée) ; les pages du site la
// prennent en compte à la génération suivante.
(function () {
"use strict";
const racine = document.getElementById("editeur");
if (!racine || !window.L) return;
const voyage = racine.dataset.voyage;
const $ = id => document.getElementById(id);
let carte = null, donnees = null, coucheTraces, coucheMedias, coucheZones;
let brouillon = null, modeZone = false, aPlacer = null, cadree = false;
// --- Échanges avec le serveur -------------------------------------------
function message(html) {
const m = $("messages");
m.innerHTML = html;
clearTimeout(window._rtgMessage);
window._rtgMessage = setTimeout(() => { m.innerHTML = ""; }, 6000);
}
function lireReponse(r) {
const redirection = r.headers.get("HX-Redirect");
if (redirection) { location.href = redirection; throw new Error("session"); }
if (!(r.headers.get("Content-Type") || "").includes("json")) {
return r.text().then(t => { message(t); throw new Error("refus"); });
}
return r.json();
}
function poster(url, params) {
return fetch(url, {
method: "POST",
headers: { "HX-Request": "true", "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(Object.assign({ v: voyage }, params))
}).then(lireReponse);
}
function etat(texte) {
$("ed-etat").textContent = texte;
}
function charger() {
return fetch("/voyage/carte.json?v=" + encodeURIComponent(voyage), { headers: { "HX-Request": "true" } })
.then(lireReponse)
.then(d => { donnees = d; remplirJours(); dessinerTout(); });
}
// --- Carte ----------------------------------------------------------------
function initialiser() {
$("ed-carte").innerHTML = "";
carte = L.map("ed-carte");
const plan = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{ maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(carte);
const satellite = L.tileLayer(
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
{ maxZoom: 19, attribution: "Esri World Imagery" });
L.control.layers({ "Plan": plan, "Satellite": satellite }).addTo(carte);
carte.setView([46.5, 2.5], 5);
coucheTraces = L.featureGroup().addTo(carte);
coucheZones = L.featureGroup().addTo(carte);
coucheMedias = L.featureGroup().addTo(carte);
carte.on("click", clicCarte);
$("ed-jour").addEventListener("change", () => { cadree = false; dessinerTout(); });
$("ed-photos").addEventListener("change", dessinerMedias);
$("ed-videos").addEventListener("change", dessinerMedias);
$("ed-nouvelle-zone").addEventListener("click", commencerZone);
$("ed-zone-annuler").addEventListener("click", finirZone);
$("ed-zone-ok").addEventListener("click", enregistrerZone);
$("ed-rayon").addEventListener("input", () => {
$("ed-rayon-texte").textContent = $("ed-rayon").value + " m";
if (brouillon) brouillon.setRadius(+$("ed-rayon").value);
});
document.addEventListener("keydown", e => {
if (e.key === "Escape") { aPlacer = null; finirZone(); mode(""); }
});
document.body.addEventListener("tache-finie", () => charger());
charger();
}
function remplirJours() {
const choix = $("ed-jour"), actuel = choix.value;
choix.length = 1;
donnees.jours.forEach(j => choix.add(new Option(j.nom, j.jour)));
choix.value = actuel;
}
function jourChoisi() { return $("ed-jour").value; }
function dessinerTout() {
dessinerTraces();
dessinerZones();
dessinerMedias();
if (!cadree) {
const b = coucheTraces.getLayers().length ? coucheTraces.getBounds()
: (coucheMedias.getLayers().length ? coucheMedias.getBounds() : null);
if (b && b.isValid()) carte.fitBounds(b, { padding: [30, 30] });
cadree = true;
}
}
function dessinerTraces() {
coucheTraces.clearLayers();
const jour = jourChoisi();
donnees.couches.forEach(c => {
if (jour && c.jour !== jour) return;
c.segments.forEach(s => L.polyline(s, { color: c.couleur, weight: 4, opacity: 0.85, interactive: false })
.addTo(coucheTraces));
});
}
function icone(m) {
return L.divIcon({ className: "", iconSize: [16, 16], iconAnchor: [8, 8],
html: '<span class="mk ' + m.type + (m.deplace ? " deplace" : "") + '"></span>' });
}
function popup(m) {
const d = document.createElement("div");
d.className = "ed-popup";
if (m.vignette) {
const img = document.createElement("img");
img.src = m.vignette; img.alt = ""; img.loading = "lazy";
d.append(img);
}
const t = document.createElement("div");
t.textContent = m.fichier + " · " + m.jour + " " + m.heure;
d.append(t);
if (m.deplace) {
const b = document.createElement("button");
b.className = "petit";
b.textContent = "Revenir à la position d'origine";
b.addEventListener("click", () => { carte.closePopup(); enregistrerPosition(m.fichier, "", ""); });
d.append(b);
}
return d;
}
function dessinerMedias() {
coucheMedias.clearLayers();
const jour = jourChoisi(), photos = $("ed-photos").checked, videos = $("ed-videos").checked;
const sans = [];
donnees.medias.forEach(m => {
if (jour && m.jour !== jour) return;
if ((m.type === "photo" && !photos) || (m.type === "video" && !videos)) return;
if (m.lat === null) { sans.push(m); return; }
L.marker([m.lat, m.lon], { icon: icone(m), draggable: true, title: m.fichier })
.bindPopup(() => popup(m), { minWidth: 200 })
.on("dragend", e => {
const p = e.target.getLatLng();
enregistrerPosition(m.fichier, p.lat.toFixed(6), p.lng.toFixed(6));
})
.addTo(coucheMedias);
});
const liste = $("ed-sans");
liste.innerHTML = "";
$("ed-nb-sans").textContent = sans.length;
sans.forEach(m => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "#carte";
a.textContent = (m.type === "photo" ? "📷 " : "🎬 ") + m.fichier;
a.title = m.jour + " " + m.heure;
a.addEventListener("click", e => {
e.preventDefault();
finirZone();
aPlacer = m.fichier;
mode("Cliquez sur la carte pour placer " + m.fichier + " (Échap pour annuler)");
});
const quand = document.createElement("span");
quand.className = "discret";
quand.textContent = " " + m.jour + " " + m.heure;
li.append(a, quand);
liste.append(li);
});
if (!sans.length) liste.innerHTML = '<li class="vide">Aucun</li>';
}
function enregistrerPosition(fichier, lat, lon) {
poster("/voyage/position", { fichier: fichier, lat: lat, lon: lon }).then(r => {
if (r.media) {
const i = donnees.medias.findIndex(m => m.fichier === fichier);
if (i >= 0) donnees.medias[i] = r.media;
}
dessinerMedias();
etat("✓ " + fichier + (lat ? " placé" : " remis à sa position d'origine")
+ ". Régénérez le voyage pour mettre le site à jour.");
}).catch(() => dessinerMedias());
}
// --- Zones masquées -------------------------------------------------------
function dessinerZones() {
coucheZones.clearLayers();
const liste = $("ed-zones");
liste.innerHTML = "";
const ajouter = (z, i, portee) => {
const site = portee === "site";
const cercle = L.circle([z[0], z[1]], {
radius: z[2], color: site ? "#7c3aed" : "#dc2626", weight: 2, fillOpacity: 0.15,
dashArray: site ? "6 6" : null
}).addTo(coucheZones);
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "#carte";
a.textContent = (site ? "Zone privée" : "Zone") + " · " + z[2] + " m";
a.addEventListener("click", e => { e.preventDefault(); carte.fitBounds(cercle.getBounds(), { maxZoom: 16 }); });
const b = document.createElement("button");
b.className = "petit";
b.textContent = "Supprimer";
b.addEventListener("click", () => {
poster("/voyage/masque", { action: "supprimer", portee: portee, index: i }).then(r => {
donnees.masques = r.masques; donnees.zones = r.zones; dessinerZones();
etat("✓ Zone supprimée. Régénérez " + (site ? "les voyages" : "le voyage") + " pour mettre le site à jour.");
});
});
li.append(a, " ", b);
if (site) li.title = "Retirée de tous les voyages (site.toml)";
liste.append(li);
};
donnees.masques.forEach((z, i) => ajouter(z, i, "voyage"));
donnees.zones.forEach((z, i) => ajouter(z, i, "site"));
}
function commencerZone() {
aPlacer = null;
modeZone = true;
mode("Cliquez au centre de la zone à masquer ; un nouveau clic la déplace.");
}
function finirZone() {
modeZone = false;
if (brouillon) { brouillon.remove(); brouillon = null; }
$("ed-brouillon").hidden = true;
if (!aPlacer) mode("");
}
function enregistrerZone() {
if (!brouillon) return;
const c = brouillon.getLatLng(), site = $("ed-portee-site").checked;
poster("/voyage/masque", {
action: "ajouter", portee: site ? "site" : "voyage",
lat: c.lat.toFixed(6), lon: c.lng.toFixed(6), rayon: $("ed-rayon").value
}).then(r => {
donnees.masques = r.masques; donnees.zones = r.zones;
finirZone();
dessinerZones();
etat("✓ Zone enregistrée. Régénérez " + (site ? "les voyages" : "le voyage") + " pour mettre le site à jour.");
});
}
function clicCarte(e) {
if (aPlacer) {
const nom = aPlacer;
aPlacer = null;
mode("");
enregistrerPosition(nom, e.latlng.lat.toFixed(6), e.latlng.lng.toFixed(6));
} else if (modeZone) {
if (!brouillon) {
brouillon = L.circle(e.latlng, { radius: +$("ed-rayon").value, color: "#dc2626", weight: 2,
dashArray: "4 4", fillOpacity: 0.2 }).addTo(carte);
} else {
brouillon.setLatLng(e.latlng);
}
$("ed-brouillon").hidden = false;
mode("Ajustez le rayon puis enregistrez ; un clic sur la carte déplace la zone.");
}
}
function mode(texte) {
const m = $("ed-mode");
m.textContent = texte;
m.hidden = !texte;
$("ed-carte").classList.toggle("en-mode", !!texte);
}
// L'onglet peut être caché au chargement : on prépare la carte à sa première ouverture.
document.addEventListener("rtg:onglet", e => {
if (e.detail !== "carte") return;
if (!carte) initialiser();
else carte.invalidateSize();
});
if (location.hash === "#carte") setTimeout(() => { if (!carte) initialiser(); }, 0);
})();
+40
View File
@@ -145,3 +145,43 @@ progress.global { height: 4px; opacity: .7; }
th, td { padding: 6px 8px; }
.carte { height: 300px; }
}
/* Connexion */
.page-connexion main.connexion { max-width: 420px; margin: 12vh auto; background: var(--carte); border: 1px solid var(--bord);
border-radius: 14px; padding: 24px 28px; }
.connexion .logo { margin: 0 0 8px; }
.connexion label { display: block; margin: 14px 0 10px; font-weight: 500; }
.connexion input[type=password] { display: block; width: 100%; margin-top: 4px; }
.discret-bouton { color: var(--discret); }
/* Carte et corrections */
.editeur { display: grid; grid-template-columns: 300px 1fr; gap: 14px; align-items: start; }
.editeur-panneau { background: var(--carte); border: 1px solid var(--bord); border-radius: 10px; padding: 8px 14px 12px;
max-height: 74vh; overflow-y: auto; }
.editeur-panneau h4 { margin: 18px 0 4px; font-size: 14px; }
.editeur-panneau label { display: block; margin: 8px 0; }
.editeur-panneau select { width: 100%; margin-top: 4px; }
.editeur-panneau input[type=range] { width: 100%; }
.ed-cases { display: flex; gap: 16px; }
.editeur-carte { height: 74vh; border-radius: 10px; border: 1px solid var(--bord); }
.editeur-carte.en-mode { cursor: crosshair; outline: 3px solid var(--accent); }
.editeur-carte.en-mode .leaflet-container, .editeur-carte.en-mode.leaflet-container { cursor: crosshair; }
.ed-mode { background: var(--attention-fond); color: var(--attention); padding: 6px 10px; border-radius: 8px; font-size: 13px; margin: 8px 0; }
.ed-brouillon { background: var(--neutre-fond); border-radius: 8px; padding: 4px 10px; margin: 8px 0; }
.ed-liste { list-style: none; padding: 0; margin: 4px 0; font-size: 13px; }
.ed-liste li { padding: 3px 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.ed-liste a { text-decoration: none; overflow-wrap: anywhere; }
.mk { display: inline-block; width: 14px; height: 14px; border-radius: 50%; border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0,0,0,.35); vertical-align: middle; }
.mk.photo { background: #ef4444; }
.mk.video { background: #2563eb; }
.mk.deplace { border-color: #f59e0b; box-shadow: 0 0 0 2px #f59e0b; }
.leaflet-marker-draggable .mk { cursor: grab; }
.ed-popup { max-width: 240px; font-size: 12px; }
.ed-popup img { display: block; max-width: 240px; max-height: 180px; border-radius: 6px; margin-bottom: 4px; }
.ed-popup button { margin-top: 6px; }
@media (max-width: 800px) {
.editeur { grid-template-columns: 1fr; }
.editeur-panneau { max-height: none; }
.editeur-carte { height: 60vh; }
}
+1
View File
@@ -18,6 +18,7 @@
s.classList.toggle("actif", s.id === "onglet-" + (trouve ? nom : liens[0].getAttribute("href").slice(1)));
});
document.querySelectorAll(".onglet.actif .carte").forEach(cadrer);
document.dispatchEvent(new CustomEvent("rtg:onglet", { detail: trouve ? nom : "" }));
}
window.addEventListener("hashchange", afficherOnglet);
document.addEventListener("DOMContentLoaded", afficherOnglet);