Spaces:
Running
Running
Claude Haiku 5.5
feat(logistique): carte de renseignement — liens adresses/IP/tel/email/piece entre clients et garants
1030d8c Download src/modules/carte_html.py from klydekushy/Vortex-Flux: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/carte_html.py
- Command line
-
hf download hf://spaces/klydekushy/Vortex-Flux/src/modules/carte_html.py
-
curl -L -o carte_html.py https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/carte_html.py
16 kB
| """ | |
| CARTE DE RENSEIGNEMENT — page Mapbox autonome (fond noir, style poste de commandement) | |
| ======================================================================================= | |
| Tout se passe DANS la page (aucun aller-retour Streamlit, donc aucune lecture Sheets au | |
| clic) : clic sur un point -> dossier du lieu (personnes, IP, telephones...) + traits vers | |
| tous les lieux relies ; recherche d'une IP / adresse / telephone / nom ; liste des alertes. | |
| Donnees : sortie de core.liens_logistique.analyser() -> donnees_carte() -> construire_html(). | |
| Les textes viennent de saisies utilisateur : ils sont toujours inseres par textContent | |
| (jamais innerHTML) et le JSON est protege contre la fermeture de balise <script>. | |
| """ | |
| import json | |
| STYLE_MAPBOX = "mapbox://styles/mapbox/dark-v11" | |
| def donnees_carte(resultat: dict) -> dict: | |
| """Ajoute a chaque lieu une chaine de recherche en minuscules.""" | |
| lieux = [] | |
| for l in resultat["lieux"]: | |
| morceaux = [l["label"]] | |
| for m in l["membres"]: | |
| morceaux += [m["id"], m["nom"], *m["tel"], *m["email"], *m["ip"], *m["employeur"], *m["piece"]] | |
| lieux.append({**l, "s": " | ".join(str(x) for x in morceaux if x).lower()}) | |
| return {"lieux": lieux, "groupes": resultat["groupes"], "stats": resultat["stats"]} | |
| def construire_html(token: str, donnees: dict, hauteur: int = 760) -> str: | |
| charge = json.dumps(donnees, ensure_ascii=False).replace("</", "<\\/") | |
| return (_MODELE.replace("__DATA__", charge) | |
| .replace("__TOKEN__", json.dumps(token)) | |
| .replace("__STYLE__", json.dumps(STYLE_MAPBOX)) | |
| .replace("__H__", str(hauteur))) | |
| _MODELE = r"""<!DOCTYPE html> | |
| <html><head><meta charset="utf-8"> | |
| <script src="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js"></script> | |
| <link href="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css" rel="stylesheet"> | |
| <style> | |
| html,body{margin:0;padding:0;background:#000;color:#cfcfcf;font-family:'Courier New',monospace;font-size:12px} | |
| #wrap{position:relative;height:__H__px;border:1px solid #262626;background:#000} | |
| #map{position:absolute;inset:0} | |
| .hud{position:absolute;background:rgba(0,0,0,.88);border:1px solid #2b2b2b;z-index:5} | |
| #left{top:10px;left:10px;width:300px;max-height:calc(100% - 20px);display:flex;flex-direction:column} | |
| #right{top:10px;right:10px;width:360px;max-height:calc(100% - 20px);overflow:auto;display:none} | |
| #legend{bottom:28px;left:10px;padding:7px 9px;font-size:10px;line-height:1.7;color:#8f8f8f} | |
| .tt{font-size:10px;letter-spacing:2px;color:#f2c94c;padding:7px 9px;border-bottom:1px solid #2b2b2b;text-transform:uppercase} | |
| #q{background:#050505;border:0;border-bottom:1px solid #2b2b2b;color:#f0f0f0;width:100%;box-sizing:border-box; | |
| padding:9px;font-family:inherit;font-size:12px;outline:none} | |
| #q:focus{border-bottom-color:#f2c94c} | |
| #nres{padding:4px 9px;color:#8f8f8f;font-size:10px;min-height:14px} | |
| #alertes{overflow:auto;flex:1} | |
| .al{padding:6px 9px;border-bottom:1px solid #141414;cursor:pointer;display:flex;gap:7px;align-items:baseline} | |
| .al:hover,.rel:hover{background:#101010} | |
| .al .v,.rel .v{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dedede} | |
| .b{font-size:9px;padding:1px 5px;border:1px solid;letter-spacing:1px;white-space:nowrap} | |
| .sec{padding:6px 9px;font-size:10px;letter-spacing:2px;color:#8f8f8f;border-top:1px solid #2b2b2b; | |
| border-bottom:1px solid #141414;text-transform:uppercase} | |
| .pers{padding:8px 9px;border-bottom:1px solid #141414} | |
| .pers .nom{color:#f0f0f0;font-size:12px} | |
| .kv{display:flex;gap:6px;margin-top:2px;color:#8f8f8f;font-size:11px;flex-wrap:wrap} | |
| .kv .k{width:62px;color:#6a6a6a;flex:none} | |
| .val{color:#cfcfcf;cursor:pointer;border-bottom:1px dotted #444} | |
| .val:hover{color:#f2c94c;border-color:#f2c94c} | |
| .rel{padding:7px 9px;border-bottom:1px solid #141414;cursor:pointer} | |
| .rel .l1{display:flex;gap:7px;align-items:baseline} | |
| .rel .l2{color:#6a6a6a;font-size:10px;margin-top:2px} | |
| .btn{display:inline-block;margin:6px 9px;padding:4px 9px;border:1px solid #444;color:#cfcfcf;cursor:pointer;font-size:10px;letter-spacing:1px} | |
| .btn:hover{border-color:#f2c94c;color:#f2c94c} | |
| .x{float:right;cursor:pointer;color:#8f8f8f}.x:hover{color:#f2c94c} | |
| .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px} | |
| .ln{display:inline-block;width:16px;height:0;border-top:2px solid;margin-right:5px;vertical-align:middle} | |
| .mapboxgl-ctrl-attrib{background:rgba(0,0,0,.6)!important} | |
| ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-thumb{background:#2b2b2b} | |
| </style></head><body> | |
| <div id="wrap"> | |
| <div id="map"></div> | |
| <div id="left" class="hud"> | |
| <div class="tt">Recherche · IP / adresse / tél / nom</div> | |
| <input id="q" placeholder="ex. 41.82.10.5 · 77 123 45 67 · Fass · CLI-2026-0001" autocomplete="off"> | |
| <div id="nres"></div> | |
| <div class="tt">Liens détectés</div> | |
| <div id="alertes"></div> | |
| </div> | |
| <div id="right" class="hud"></div> | |
| <div id="legend" class="hud"></div> | |
| </div> | |
| <script> | |
| const DATA = __DATA__; | |
| mapboxgl.accessToken = __TOKEN__; | |
| const NIV = {critique:'#e05a4f', eleve:'#f2994a', attention:'#f2c94c', info:'#9aa7b2', normal:'#6fcf97'}; | |
| const NIV_LIB = {critique:'CRITIQUE', eleve:'ÉLEVÉ', attention:'ATTENTION', info:'INFO', normal:'RAS'}; | |
| const TYPE_COL = {tel:'#f2c94c', email:'#c792ea', ip:'#4fc3f7', piece:'#e05a4f', adresse:'#ffffff', employeur:'#8f8f8f'}; | |
| const LIEUX = DATA.lieux, GROUPES = DATA.groupes; | |
| const G = Object.fromEntries(GROUPES.map(g => [g.gid, g])); | |
| const L = Object.fromEntries(LIEUX.map(l => [l.id, l])); | |
| const $ = id => document.getElementById(id); | |
| function el(tag, cls, txt) { const e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; } | |
| function badge(txt, col) { const b = el('span', 'b', txt); b.style.color = col; b.style.borderColor = col; return b; } | |
| const map = new mapboxgl.Map({container:'map', style:__STYLE__, center:[-17.45, 14.7], zoom:11, attributionControl:true}); | |
| map.addControl(new mapboxgl.NavigationControl({showCompass:false}), 'bottom-right'); | |
| map.addControl(new mapboxgl.ScaleControl({unit:'metric'}), 'bottom-right'); | |
| const vide = () => ({type:'FeatureCollection', features:[]}); | |
| const pointsFC = () => ({type:'FeatureCollection', features: LIEUX.map(l => ({ | |
| type:'Feature', geometry:{type:'Point', coordinates:[l.lon, l.lat]}, | |
| properties:{id:l.id, color:NIV[l.niveau]||NIV.normal, r:6+Math.min(l.n,6)*2.2, n:l.n, decl:l.declare, risque:l.risque}}))}); | |
| const ptsOf = ids => ({type:'FeatureCollection', features: ids.map(i => ({type:'Feature', | |
| geometry:{type:'Point', coordinates:[L[i].lon, L[i].lat]}, properties:{id:i, r:6+Math.min(L[i].n,6)*2.2}}))}); | |
| let selectedId = null, selectedGid = null; | |
| map.on('load', () => { | |
| map.addSource('lieux', {type:'geojson', data:pointsFC()}); | |
| map.addSource('liens', {type:'geojson', data:vide()}); | |
| map.addSource('rel', {type:'geojson', data:vide()}); | |
| map.addSource('match', {type:'geojson', data:vide()}); | |
| map.addLayer({id:'liens-halo', type:'line', source:'liens', layout:{'line-cap':'round'}, | |
| paint:{'line-color':['get','color'], 'line-width':7, 'line-opacity':.18, 'line-blur':3}}); | |
| map.addLayer({id:'liens', type:'line', source:'liens', layout:{'line-cap':'round'}, | |
| paint:{'line-color':['get','color'], 'line-width':2.2, | |
| 'line-opacity':['case', ['==',['get','type'],'employeur'], .45, .95]}}); | |
| map.addLayer({id:'halo', type:'circle', source:'lieux', | |
| paint:{'circle-radius':['+',['get','r'],7], 'circle-color':['get','color'], 'circle-opacity':.14, 'circle-blur':.8}}); | |
| map.addLayer({id:'risque', type:'circle', source:'lieux', filter:['==',['get','risque'],true], | |
| paint:{'circle-radius':['+',['get','r'],4], 'circle-color':'rgba(0,0,0,0)', 'circle-stroke-color':'#e05a4f', | |
| 'circle-stroke-width':1.5, 'circle-stroke-opacity':.9}}); | |
| map.addLayer({id:'pts', type:'circle', source:'lieux', | |
| paint:{'circle-radius':['get','r'], 'circle-color':['get','color'], | |
| 'circle-opacity':['case',['get','decl'],.92,.18], 'circle-stroke-color':['get','color'], | |
| 'circle-stroke-width':2, 'circle-stroke-opacity':1}}); | |
| map.addLayer({id:'rel', type:'circle', source:'rel', | |
| paint:{'circle-radius':['+',['get','r'],5], 'circle-color':'rgba(0,0,0,0)', 'circle-stroke-color':'#ffffff', | |
| 'circle-stroke-width':1.5, 'circle-stroke-opacity':.9}}); | |
| map.addLayer({id:'match', type:'circle', source:'match', | |
| paint:{'circle-radius':['+',['get','r'],10], 'circle-color':'rgba(0,0,0,0)', 'circle-stroke-color':'#f2c94c', | |
| 'circle-stroke-width':2.5, 'circle-stroke-opacity':1}}); | |
| map.addLayer({id:'nb', type:'symbol', source:'lieux', filter:['>',['get','n'],1], | |
| layout:{'text-field':['to-string',['get','n']], 'text-font':['DIN Pro Bold','Arial Unicode MS Bold'], 'text-size':11, 'text-allow-overlap':true}, | |
| paint:{'text-color':'#000000'}}); | |
| map.on('click', 'pts', e => { select(e.features[0].properties.id, null); }); | |
| map.on('click', e => { if (!map.queryRenderedFeatures(e.point, {layers:['pts']}).length) deselect(); }); | |
| map.on('mouseenter', 'pts', () => map.getCanvas().style.cursor = 'pointer'); | |
| map.on('mouseleave', 'pts', () => map.getCanvas().style.cursor = ''); | |
| cadrer(LIEUX.map(l => l.id), false); | |
| }); | |
| function cadrer(ids, animer) { | |
| if (!ids.length) return; | |
| const b = new mapboxgl.LngLatBounds(); | |
| ids.forEach(i => b.extend([L[i].lon, L[i].lat])); | |
| map.fitBounds(b, {padding:{top:60, bottom:60, left:340, right:selectedId != null ? 400 : 60}, maxZoom:ids.length === 1 ? 15 : 13, | |
| duration: animer === false ? 0 : 700}); | |
| } | |
| function lignes(id, groupes) { | |
| const feats = [], vus = new Set(), a = L[id]; | |
| groupes.forEach(g => g.lieux.forEach(li => { | |
| if (li === id) return; | |
| const cle = g.type + '|' + li; | |
| if (vus.has(cle)) return; vus.add(cle); | |
| feats.push({type:'Feature', geometry:{type:'LineString', coordinates:[[a.lon, a.lat],[L[li].lon, L[li].lat]]}, | |
| properties:{type:g.type, color:TYPE_COL[g.type]||'#fff'}}); | |
| })); | |
| return {type:'FeatureCollection', features:feats}; | |
| } | |
| function select(id, gid) { | |
| selectedId = id; selectedGid = gid || null; | |
| const lieu = L[id]; | |
| const groupes = (selectedGid ? [G[selectedGid]] : lieu.groupes.map(x => G[x])); | |
| map.getSource('liens').setData(lignes(id, groupes)); | |
| const lies = [...new Set(groupes.flatMap(g => g.lieux).filter(x => x !== id))]; | |
| map.getSource('rel').setData(ptsOf([id, ...lies])); | |
| cadrer([id, ...lies], true); | |
| dossier(lieu, groupes); | |
| } | |
| function deselect() { | |
| selectedId = null; selectedGid = null; | |
| map.getSource('liens').setData(vide()); map.getSource('rel').setData(vide()); | |
| $('right').style.display = 'none'; | |
| } | |
| function chercher(txt) { $('q').value = txt; recherche(true); } | |
| function valeurs(parent, cle, liste) { | |
| if (!liste || !liste.length) return; | |
| const kv = el('div', 'kv'); kv.appendChild(el('span', 'k', cle)); | |
| liste.forEach((v, i) => { const s = el('span', 'val', v); s.title = 'Chercher partout'; s.onclick = () => chercher(v); kv.appendChild(s); }); | |
| parent.appendChild(kv); | |
| } | |
| function dossier(lieu, groupes) { | |
| const p = $('right'); p.textContent = ''; p.style.display = 'block'; | |
| const t = el('div', 'tt'); const x = el('span', 'x', '✕'); x.onclick = deselect; t.appendChild(x); | |
| t.appendChild(document.createTextNode('Dossier du lieu')); p.appendChild(t); | |
| const h = el('div', 'pers'); h.appendChild(el('div', 'nom', lieu.label)); | |
| const kv = el('div', 'kv'); kv.appendChild(badge(NIV_LIB[lieu.niveau] || 'RAS', NIV[lieu.niveau] || NIV.normal)); | |
| kv.appendChild(el('span', '', lieu.lat.toFixed(5) + ', ' + lieu.lon.toFixed(5))); | |
| kv.appendChild(el('span', '', lieu.declare ? 'adresse déclarée' : 'position observée uniquement')); | |
| h.appendChild(kv); p.appendChild(h); | |
| p.appendChild(el('div', 'sec', 'Personnes (' + lieu.membres.length + ')')); | |
| lieu.membres.forEach(m => { | |
| const c = el('div', 'pers'); const l1 = el('div', 'kv'); l1.style.marginTop = '0'; | |
| l1.appendChild(badge(m.kind === 'garant' ? 'GARANT' : 'CLIENT', m.kind === 'garant' ? '#c792ea' : '#cfcfcf')); | |
| l1.appendChild(el('span', 'val', m.id)).onclick = () => chercher(m.id); | |
| if (m.statut && m.statut !== 'AUCUN') l1.appendChild(badge(m.statut, ['EN_RETARD','LITIGE'].includes(m.statut) ? '#e05a4f' : '#8f8f8f')); | |
| c.appendChild(l1); c.appendChild(el('div', 'nom', m.nom || '—')); | |
| c.appendChild(el('div', 'kv', m.roles.join(' · '))); | |
| valeurs(c, 'TÉL', m.tel); valeurs(c, 'EMAIL', m.email); valeurs(c, 'IP', m.ip); | |
| valeurs(c, 'EMPLOYEUR', m.employeur); valeurs(c, 'PIÈCE', m.piece); | |
| p.appendChild(c); | |
| }); | |
| const tous = lieu.groupes.map(x => G[x]); | |
| p.appendChild(el('div', 'sec', 'Relations (' + tous.length + ')')); | |
| if (!tous.length) p.appendChild(el('div', 'pers', 'Aucun recoupement : ce lieu n’est relié à aucun autre.')); | |
| tous.forEach(g => { | |
| const r = el('div', 'rel'); const l1 = el('div', 'l1'); | |
| l1.appendChild(badge(g.libelle, TYPE_COL[g.type] || '#fff')); | |
| l1.appendChild(el('span', 'v', g.valeur)); | |
| l1.appendChild(badge(NIV_LIB[g.niveau], NIV[g.niveau])); | |
| r.appendChild(l1); | |
| r.appendChild(el('div', 'l2', g.entites.map(e => e.id + ' ' + (e.nom || '')).join(' · ') + | |
| (g.garant_emprunteur ? ' ⚠ garant ↔ emprunteur' : '') + ' · ' + g.lieux.length + ' lieu(x)')); | |
| r.onclick = () => select(selectedId, g.gid); | |
| if (selectedGid === g.gid) r.style.background = '#161616'; | |
| p.appendChild(r); | |
| }); | |
| if (selectedGid) { const b = el('div', 'btn', 'AFFICHER TOUTES LES RELATIONS'); b.onclick = () => select(selectedId, null); p.appendChild(b); } | |
| } | |
| // ---- alertes --------------------------------------------------------------- | |
| (function () { | |
| const box = $('alertes'); | |
| const liste = GROUPES.filter(g => g.niveau !== 'info'); | |
| if (!liste.length) box.appendChild(el('div', 'pers', 'Aucun recoupement détecté.')); | |
| liste.slice(0, 60).forEach(g => { | |
| const a = el('div', 'al'); | |
| a.appendChild(badge(g.libelle, TYPE_COL[g.type] || '#fff')); | |
| a.appendChild(el('span', 'v', g.valeur)); | |
| a.appendChild(badge(String(g.score), NIV[g.niveau])); | |
| a.title = g.entites.map(e => e.id + ' ' + (e.nom || '')).join(' / '); | |
| a.onclick = () => select(g.lieux[0], g.gid); | |
| box.appendChild(a); | |
| }); | |
| })(); | |
| // ---- recherche -------------------------------------------------------------- | |
| function recherche(ouvrir) { | |
| const q = $('q').value.trim().toLowerCase(); | |
| if (q.length < 2) { map.getSource('match').setData(vide()); $('nres').textContent = ''; return; } | |
| const ids = LIEUX.filter(l => l.s.includes(q)).map(l => l.id); | |
| map.getSource('match').setData(ptsOf(ids)); | |
| $('nres').textContent = ids.length + ' lieu(x) trouvé(s)'; | |
| if (ouvrir && ids.length === 1) select(ids[0], null); | |
| else if (ouvrir && ids.length > 1) { deselect(); cadrer(ids, true); } | |
| } | |
| $('q').addEventListener('input', () => recherche(false)); | |
| $('q').addEventListener('keydown', e => { if (e.key === 'Enter') recherche(true); }); | |
| // ---- legende --------------------------------------------------------------- | |
| (function () { | |
| const lg = $('legend'); | |
| const l1 = el('div'); Object.keys(NIV).forEach(k => { | |
| const d = el('span', 'dot'); d.style.background = NIV[k]; l1.appendChild(d); l1.appendChild(document.createTextNode(NIV_LIB[k] + ' ')); | |
| }); lg.appendChild(l1); | |
| const l2 = el('div'); ['tel','email','ip','piece','adresse','employeur'].forEach(k => { | |
| const d = el('span', 'ln'); d.style.borderColor = TYPE_COL[k]; l2.appendChild(d); | |
| l2.appendChild(document.createTextNode(({tel:'TÉL',email:'EMAIL',ip:'IP',piece:'PIÈCE',adresse:'ADR.',employeur:'EMPL.'})[k] + ' ')); | |
| }); lg.appendChild(l2); | |
| lg.appendChild(el('div', '', 'Plein = adresse déclarée · creux = position observée · anneau rouge = prêt en retard/litige')); | |
| })(); | |
| </script></body></html> | |
| """ | |