{"libraries":[{"name":"trace-smoother","title":"Trace smoother BRouter","category":"cartography","status":"stable","description":"Map-match des polylines sparse (Strava mapPolyline, GPX,\nOverland pings) sur le réseau OSM via BRouter, avec fidelity\nchecks contre une polyline dense de reference. Détecte les\ncoupures GPS réelles (voiture/train/bateau) et split en\nsous-tracés honnêtes (pas de fausse ligne routée entre).\n","keywords":["polyline","gpx","strava","overland","brouter","trace","map-matching","route","smoothing","mapPolyline","detailPolyline","osm","leaflet","cartography"],"use_when":["Une polyline Strava ressemble à une nuée de points sur la carte","Tracer un parcours suivant les routes OSM réelles","Smooth des pings GPS sparse (Overland 1/min) en tracé dense","Détecter les coupures GPS réelles dans un GPX (voiture/train)"],"avoid_when":["Tu veux la ground truth GPS exacte (utiliser detailPolyline brut)","Pas d'instance BRouter accessible (l'app fallback gracefully mais sans gain)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/trace-smoother","local_paths":["~/.claude/skills/atelier-resources/packages/trace-smoother","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/trace-smoother"],"install":{"degit":"npx degit ateliersam86/atelier-resources/packages/trace-smoother lib/trace-smoother\n","copy":"cp -r ~/.claude/skills/atelier-resources/packages/trace-smoother $PROJECT_ROOT/lib/\n"},"requirements":[{"id":"brouter","title":"BRouter instance HTTP","env_var":"BROUTER_URL","default":"http://192.168.1.72:17777/brouter","check":"curl -sf \"$BROUTER_URL/profile/trekking\" -o /dev/null","install":"# Self-hosted Docker (recommandé pour les projets Atelier)\ndocker run -d --name brouter -p 17777:17777 \\\n  -v ~/brouter/segments:/brouter/segments4 \\\n  -v ~/brouter/profiles2:/brouter/profiles2 \\\n  abrensch/brouter\n","public_fallback":"https://brouter.de/brouter"},{"id":"polyline_npm","title":"\"@mapbox/polyline\" pour encode/decode","check":"grep -q \"@mapbox/polyline\" package.json","install":"npm install @mapbox/polyline"}],"usage_example":"import { smoothWithCache, createDiskCache } from \"@/lib/trace-smoother\";\nimport polyline from \"@mapbox/polyline\";\n\nconst sparsePoints = polyline.decode(stravaMapPolyline)\n  .map(([lat, lon]) => ({ lat, lon }));\nconst referenceDense = polyline.decode(stravaDetailPolyline)\n  .map(([lat, lon]) => ({ lat, lon }));\n\nconst cache = createDiskCache({\n  rootDir: \"./cache/traces\",\n  namespace: \"trip-abc\",\n  id: \"segment-42\",\n});\n\nconst result = await smoothWithCache(sparsePoints, {\n  brouterUrl: process.env.BROUTER_URL ?? \"http://localhost:17777/brouter\",\n  profile: \"trekking\",        // ou fastbike, hiking-mountain\n  reference: referenceDense,  // ground truth pour fidelity\n  cache,\n});\n\n// result.subTraces: array de { points, source: \"brouter\"|\"raw\" }\n// result.diagnostics: { gapsDetected, sourceKm, smoothKm, rejects }\n","api_surface":["smoothTrace(sparse, opts) — algo pur, pas de cache","smoothWithCache(sparse, opts & { cache }) — convenience wrapper","createDiskCache({ rootDir, namespace, id }) — cache disk simple","callBrouter(waypoints, opts) — bas niveau","splitByGaps, haversineKm, polylineKm — geo primitives"],"docs_url":"./trace-smoother/README.md","added":"2026-05-12","last_validated":"2026-05-12"},{"name":"use-live-data","title":"Hook React partagé pour /live polling","category":"live","status":"stable","description":"Hook TanStack Query qui mutualise le polling /live entre N\nconsumers React. Résout le problème : 14 composants qui poll\nindépendamment le même endpoint saturent la queue HTTP/1.1 du\nbrowser (max 6 connexions) et stallent les chunks critiques.\n1 fetch partagé, tout le monde lit depuis le cache TanStack.\n","keywords":["live","polling","tanstack","react-query","hook","shared","dedup","tracking","sse-alternative","overland"],"use_when":["Plusieurs composants React lisent le même endpoint /live ou similaire","Tu veux mutualiser un polling périodique entre N consumers","Le browser stall sur 6+ requêtes concurrentes"],"avoid_when":["Le polling est unique (1 seul consumer)","Tu as besoin de WebSocket / SSE (utiliser EventSource direct)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/useLiveData.ts","local_paths":["~/.claude/skills/atelier-resources/packages/useLiveData.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/useLiveData.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-resources/packages/useLiveData.ts $PROJECT_ROOT/lib/\n","degit":"# 1 fichier — copie manuelle ou via curl\ncurl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-resources/main/packages/useLiveData.ts \\\n  -o lib/useLiveData.ts\n"},"requirements":[{"id":"tanstack_query","title":"@tanstack/react-query installé","check":"grep -q \"@tanstack/react-query\" package.json","install":"npm install @tanstack/react-query"},{"id":"query_provider","title":"QueryClientProvider à la racine de l'app","check":"grep -rq \"QueryClientProvider\" app/ src/ 2>/dev/null","install":"# À ajouter dans ton app root (Next.js : app/providers.tsx)\n# Pattern standard TanStack Query setup\n"}],"usage_example":"import { useLiveData } from \"@/lib/useLiveData\";\n\nfunction MyWidget({ slug }) {\n  const liveQuery = useLiveData(slug, { enabled: true });\n  const data = liveQuery.data ?? null;\n  return <div>{data?.last?.timestamp}</div>;\n}\n\n// 14 instances de MyWidget = 1 seul fetch /live partagé.\n","api_surface":["useLiveData(slug, { enabled? }) → UseQueryResult<LiveData>","type LiveData (flexible, key/value)"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"use-geocode-reverse","title":"Hook React partagé pour reverse-geocode cached","category":"live","status":"stable","description":"Hook TanStack Query qui partage les fetches /api/geocode/reverse\nentre N composants. Cache keyé sur (lat, lon) arrondis à 5 décimales\n(~1m précision), staleTime: Infinity (les labels OSM ne changent\njamais). Résout le problème : 84 fetches geocode au load → 14\nuniques après dédup → queue HTTP libérée.\n","keywords":["geocode","geocoding","reverse-geocode","nominatim","tanstack","react-query","hook","shared","dedup","location","label"],"use_when":["Plusieurs composants affichent le label d'une même coord (start/end de jour, etc.)","Tu fais 30+ fetches /geocode/reverse au load d'une page","Le browser stall sur les requêtes parallèles"],"avoid_when":["Un seul reverse-geocode dans toute la page (overkill)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/useGeocodeReverse.ts","local_paths":["~/.claude/skills/atelier-resources/packages/useGeocodeReverse.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/useGeocodeReverse.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-resources/packages/useGeocodeReverse.ts $PROJECT_ROOT/lib/\n","degit":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-resources/main/packages/useGeocodeReverse.ts \\\n  -o lib/useGeocodeReverse.ts\n"},"requirements":[{"id":"tanstack_query","title":"@tanstack/react-query installé","check":"grep -q \"@tanstack/react-query\" package.json","install":"npm install @tanstack/react-query"},{"id":"api_endpoint","title":"Endpoint /api/geocode/reverse côté serveur (Nominatim wrapper avec cache)","check":"ls app/api/geocode/reverse/route.ts 2>/dev/null","install":"# À implémenter selon ton stack. Le hook attend la shape :\n# { label: string | null }\n"}],"usage_example":"import { useGeocodeReverse } from \"@/lib/useGeocodeReverse\";\n\nfunction DayMarker({ coord }) {\n  const label = useGeocodeReverse(coord.lat, coord.lon);\n  return <span>{label ?? \"Chargement...\"}</span>;\n}\n","api_surface":["useGeocodeReverse(lat, lon) → string | undefined"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"auto-login","title":"Auto-login pipeline (HumanCursor + Claude Vision)","category":"scraping","status":"stable","description":"Pipeline autonome pour login sur sites e-commerce avec captchas.\nDétecte le type de challenge (reCAPTCHA v2/v3/Enterprise, hCaptcha,\nCloudflare Turnstile, CF IUAM \"Just a moment\", DataDome, PerimeterX,\nFunCaptcha, GeeTest, Imperva) et route vers le bon solver :\n\n  - reCAPTCHA v2 + hCaptcha image grids  → Claude Vision + HumanCursor\n  - reCAPTCHA v3/Enterprise (score)      → Patchright stealth seul\n  - Cloudflare Turnstile + IUAM          → wait passif + click managed\n  - DataDome / PerimeterX / FunCaptcha   → flag UI Login VNC (non auto)\n\nPatchright stealth + mouvements souris Bezier humanisés\n(pyclick.HumanCurve). Vision via Claude Haiku 4.5 (Meridian wrapper\nOAuth gratuit / Anthropic API / compatible OpenAI). State.json cookies\npersistés <30j. Recordings webm + screenshots à chaque tentative pour\ndebug post-mortem. Captcha type détecté persisté en DB\n(suppliers.captcha_type) → badge UI couleur par solvability + monitoring\nd'évolution (alerte si un provider change de captcha). Fallback gracieux\nsi LLM down : flag UI Login VNC manuel. Zéro service tiers payant si\nOAuth Claude Code Max dispo.\n","keywords":["captcha","recaptcha","hcaptcha","cloudflare","turnstile","cf-iuam","datadome","perimeterx","funcaptcha","geetest","imperva","captcha-detection","captcha-solver","scraping","login","patchright","playwright","stealth","humancursor","bezier","claude-vision","prestashop","magento","fournisseur","cookies","state.json","auto-login","anti-bot"],"use_when":["Tu scrapes un fournisseur B2B (PrestaShop / Magento / custom) avec reCAPTCHA","Tu veux automatiser le login + résolution captcha sans 2captcha payant","Cookies session expirent régulièrement et tu veux auto-refresh","Sites Cloudflare bloquent Playwright vanilla → besoin Patchright stealth","Tu veux des recordings post-mortem pour debug quand un scraper fail"],"avoid_when":["Pas de wrapper Claude Code OAuth ni clé Anthropic dispo → fallback VNC manuel","Site sans captcha — httpx + cookies basiques suffisent","reCAPTCHA Enterprise v3 score-only (pas d'interaction visible)"],"repo":"github.com/ateliersam86/hub-atelier-de-sam","repo_subpath":"backend/scrapers/captcha","local_paths":["/Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/captcha","/Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/auto_login_pipeline.py"],"install":{"copy":"cp -r /Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/captcha $PROJECT_ROOT/lib/\ncp /Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/auto_login_pipeline.py $PROJECT_ROOT/lib/captcha/\n"},"requirements":[{"id":"patchright","title":"Patchright (Playwright fork stealth)","check":"python3 -c 'import patchright'","install":"pip install patchright>=1.49.0 && patchright install chrome"},{"id":"pyclick","title":"pyclick (HumanCurve Bezier trajectories)","check":"python3 -c 'from pyclick.humancurve import HumanCurve'","install":"pip install pyclick==0.0.2 && apt install python3-tk"},{"id":"pillow","title":"PIL (image downscale)","check":"python3 -c 'from PIL import Image'","install":"pip install Pillow"},{"id":"vision-llm","title":"Wrapper Claude/GPT vision (au choix)","env_var":"MERIDIAN_WRAPPERS","default":"http://192.168.1.72:3459|wrapper-op-meridian-2","check":"curl -sf \"${MERIDIAN_WRAPPERS%%,*}\" | grep -q healthy"},{"id":"xvfb","title":"Xvfb display server (pour Patchright headful en container)","check":"which Xvfb","install":"apt install xvfb"}],"usage_example":"from scrapers.captcha import HumanMouse, solve_image_grid\nfrom scrapers.auto_login_pipeline import auto_login_async\n\n# Cas 1: pipeline complet (httpx scraper)\ncookies = await auto_login_async(\n    slug=\"brico-phone\",\n    login_url=\"https://www.brico-phone.com/connexion\",\n    email=\"…\", password=\"…\",\n    target_domain=\"brico-phone.com\",\n)\nif cookies:\n    for c in cookies:\n        httpx_client.cookies.set(c[\"name\"], c[\"value\"], domain=c[\"domain\"])\n\n# Cas 2: solver vision seul (pour reCAPTCHA dans ton propre flow)\ncells = await solve_image_grid(\n    screenshot_path=\"/tmp/recaptcha.png\",\n    instruction=\"Sélectionnez toutes les images montrant des vélos\",\n    grid_size=3,\n)\n# cells = [1, 5, 7] → click ces cells dans l'iframe bframe\n\n# Cas 3: HumanCursor seul (humaniser n'importe quel click Playwright)\nmouse = HumanMouse(page)\nawait mouse.click_at(450, 320)  # trajectoire Bezier + jitter timing\n","api_surface":["auto_login_async(slug, login_url, email, pwd, target_domain) -> list[cookie] | None","recover_session_for_playwright(page, slug, login_url, …) -> bool","detect_captcha(page) -> str  # recaptcha_v2_image | hcaptcha | cloudflare_turnstile | …","captcha.solvers.solve(captcha_type, page, mouse) -> bool","captcha.solvers.solve_recaptcha_v2(page, mouse) -> bool","captcha.solvers.solve_hcaptcha(page, mouse) -> bool","captcha.solvers.solve_turnstile(page, mouse, max_wait=25) -> bool","captcha.solvers.solve_cf_iuam(page, mouse, max_wait=25) -> bool","solve_image_grid(screenshot_path, instruction, grid_size=3) -> list[int] | None","HumanMouse(page).click_at(x, y) / click_locator(loc) / type_into(sel, val)","load_fresh_state_cookies(slug, max_age_days=30) -> list[cookie] | None","save_state_cookies(slug, cookies)","persist_captcha_type(slug, captcha_type)  # UPDATE suppliers.captcha_type"],"notes":"- Recordings webm dans data/auto_login_recordings/<slug>/<timestamp>/\n  + _captcha.json (type détecté + solvability) à chaque tentative.\n- Vision provider configurable via MERIDIAN_WRAPPERS env\n  (format: \"url1|api_key1,url2|api_key2\", premier qui répond 200 wins)\n- Testé live le 2026-05-12 : Meridian-2 (port 3459, Claude Haiku 4.5)\n  résout reCAPTCHA \"feux de circulation\" en ~5s, cells [1,3,7,8] OK\n- Détection 11 types de captcha (incl. les non auto-résolvables :\n  DataDome/PerimeterX/FunCaptcha/GeeTest → flag UI VNC).\n- Map SOLVABILITY: auto / stealth_only / manual / none / unknown\n- Câblé dans 4 scrapers Hub : invoice_prestashop, foneday, mobilax,\n  jensmobiles (commits 4848579 → cb70013)\n- Lib pas encore extraite en repo dédié, vit dans le mono-repo Hub.\n  Extract en humanlogin/pip si traction (~1 jour de boulot).\n- Tests démo officiels passés (2026-05-12) :\n  * example.com                            → detect=none ✓\n  * google.com/recaptcha/api2/demo         → detect=recaptcha_v2_image ✓\n                                             solve: Claude refuse safety filter\n                                             → stealth checkbox-only suffit\n  * accounts.hcaptcha.com/demo             → detect=hcaptcha ✓\n  * nopecha.com/demo/turnstile             → detect=cloudflare_turnstile ✓\n                                             (script présent, pas de widget)\n  * 2captcha.com/demo/cloudflare-turnstile → solve fail-through correct\n                                             (token <30c → flag UI VNC)\n- Note safety Claude : refuse parfois sur les pages explicitement\n  nommées \"captcha demo\". Sur les vrais logins B2B fournisseurs (login\n  légitime pour des comptes que tu possèdes), le filter ne se déclenche\n  pas. Si ça arrive en prod : log dans llm_errors → visible UI.\n- Fix 2026-05-12 : solve_turnstile retournait True à tort quand le\n  widget n'était pas encore rendu (script loader vu mais widget lazy).\n  Remplacé wait_for(state=visible) par poll DOM 5s.\n","added":"2026-05-12","last_validated":"2026-05-12","test_sites":["https://www.google.com/recaptcha/api2/demo","https://accounts.hcaptcha.com/demo","https://nopecha.com/demo/turnstile","https://2captcha.com/demo/cloudflare-turnstile"]},{"name":"client-admin-auth","title":"Admin password client-side (sessionStorage)","category":"auth","status":"stable","description":"Helpers React pour gérer un mot de passe admin côté client :\nverify contre un endpoint serveur, stocker en sessionStorage\n(jamais en localStorage, jamais hardcodé dans le bundle),\nrelire pour les appels admin suivants. Namespace par scope\n(default + linked-trip keys) — plusieurs admin coexistent sur\nle même tab. Pattern utilisé sur tous les admin UI Atelier.\n","keywords":["admin","auth","password","sessionStorage","login","gate","middleware-client","react"],"use_when":["Créer un admin UI où l'utilisateur tape un mot de passe","Plusieurs admin scopes coexistent (Sam admin, Paul admin, etc.)","Tu veux un pattern simple sans Auth0/Clerk pour un usage perso"],"avoid_when":["Tu as besoin d'un vrai système d'auth multi-utilisateur (utiliser Clerk/Auth0)","Côté serveur seul (utiliser lib/adminAuth.ts du même projet)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/clientAdminAuth.ts","local_paths":["~/.claude/skills/atelier-resources/packages/clientAdminAuth.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/clientAdminAuth.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-resources/packages/clientAdminAuth.ts $PROJECT_ROOT/lib/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-resources/main/packages/clientAdminAuth.ts \\\n  -o lib/clientAdminAuth.ts\n"},"requirements":[{"id":"verify_endpoint","title":"Endpoint serveur POST /api/admin/verify-password","check":"ls app/api/admin/verify-password/route.ts 2>/dev/null","install":"# Wrapper côté serveur qui valide le password contre une env var\n# (timing-safe comparaison + rate-limit). Voir lib/adminAuth.ts\n# dans atelier-web-travels pour la référence d'implémentation.\n"}],"usage_example":"\"use client\";\nimport { verifyAdminPasswordAndStore, getStoredAdminPassword } from \"@/lib/clientAdminAuth\";\n\n// Gate component\nasync function handleSubmit(password: string) {\n  const { ok } = await verifyAdminPasswordAndStore(password);\n  if (ok) router.push(\"/admin/dashboard\");\n}\n\n// Subsequent admin API calls\nconst adminPassword = getStoredAdminPassword();\nawait fetch(\"/api/admin/trips/x\", {\n  headers: { \"x-admin-password\": adminPassword },\n});\n\n// Linked-trip scope (multi-admin)\nawait verifyAdminPasswordAndStore(pwd, \"paul\");\nconst paulPwd = getStoredAdminPassword(\"paul\");\n","api_surface":["verifyAdminPasswordAndStore(candidate, linkedTripKey?) → { ok, status }","getStoredAdminPassword(linkedTripKey?) → string","setStoredAdminPassword(password, linkedTripKey?)","clearStoredAdminPassword(linkedTripKey?)"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"leaflet-loader","title":"Dynamic Leaflet import + React hook","category":"cartography","status":"stable","description":"Wrapper léger pour charger Leaflet de façon dynamique (jamais\nimporté pendant SSR). Mémoise la Promise — N composants qui\nutilisent la map partagent le même import. Hook `useLeaflet()`\npour les Components React. Empêche les \"ReferenceError: window\nis not defined\" et les double-imports qui font stall les chunks\nLeaflet 20s dans la queue HTTP du browser.\n","keywords":["leaflet","react","dynamic-import","ssr","hook","map","cartography","bundle-split"],"use_when":["App Next.js/SSR avec une map Leaflet","Plusieurs composants utilisent Leaflet et tu veux mutualiser le chargement","Tu veux éviter `import 'leaflet'` direct qui plante en SSR"],"avoid_when":["Pas de SSR (tu peux import direct)","Tu utilises Mapbox/MapLibre (incompatible)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/leafletLoader.ts","local_paths":["~/.claude/skills/atelier-resources/packages/leafletLoader.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/utils/leafletLoader.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-resources/packages/leafletLoader.ts $PROJECT_ROOT/utils/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-resources/main/packages/leafletLoader.ts \\\n  -o utils/leafletLoader.ts\n"},"requirements":[{"id":"leaflet_npm","title":"leaflet installé","check":"grep -q \"\\\"leaflet\\\"\" package.json","install":"npm install leaflet && npm install --save-dev @types/leaflet"}],"usage_example":"\"use client\";\nimport { useLeaflet, loadLeaflet } from \"@/utils/leafletLoader\";\n\n// Hook React (Component pattern)\nfunction MapComponent() {\n  const leaflet = useLeaflet();\n  if (!leaflet) return <div>Loading map...</div>;\n  // use leaflet.map(), leaflet.tileLayer(), etc.\n}\n\n// Promise pattern (useEffect direct)\nuseEffect(() => {\n  let map;\n  loadLeaflet().then((leaflet) => {\n    map = leaflet.map(ref.current).setView([0, 0], 2);\n  });\n  return () => map?.remove();\n}, []);\n","api_surface":["loadLeaflet() → Promise<LeafletModule>","useLeaflet() → LeafletModule | null","type LeafletModule"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"shorten-place-label","title":"Verbose place name → 'City · Country'","category":"cartography","status":"stable","description":"Tronque les labels géocodés verbeux (\"Nice, Provence-Alpes-Côte\nd'Azur, France\") en un format compact (\"Nice · France\") via\nmapping REGION_TO_COUNTRY. Garde les régions courtes telles\nquelles (\"Alès · France\"). Pratique pour les tooltip de carte,\npilules live, badges. Couvre toutes les régions de France\nmétropolitaine + DOM-TOM, extensible par dictionnaire.\n","keywords":["geocoding","label","format","nominatim","tooltip","shorten","cartography"],"use_when":["Tu affiches des labels de lieu sur une carte/pilule (espace contraint)","Nominatim te renvoie 3 parts mais tu en veux 2"],"avoid_when":["Tu as besoin du label complet (sous-titre détail page, par ex.)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/shortenPlaceLabel.ts","local_paths":["~/.claude/skills/atelier-resources/packages/shortenPlaceLabel.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/utils/shortenPlaceLabel.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-resources/packages/shortenPlaceLabel.ts $PROJECT_ROOT/utils/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-resources/main/packages/shortenPlaceLabel.ts \\\n  -o utils/shortenPlaceLabel.ts\n"},"usage_example":"import { shortenPlaceLabel } from \"@/utils/shortenPlaceLabel\";\n\nshortenPlaceLabel(\"Nice, Provence-Alpes-Côte d'Azur, France\");\n// → \"Nice · France\"\n\nshortenPlaceLabel(\"Alès, Occitanie\");\n// → \"Alès · France\" (via REGION_TO_COUNTRY)\n\nshortenPlaceLabel(\"Paris\");\n// → \"Paris\"\n","api_surface":["shortenPlaceLabel(raw: string) → string"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"poi-lookup","title":"POI nearest via Overpass OSM + cache 24h","category":"cartography","status":"stable","description":"Cherche le POI nommé le plus pertinent dans un rayon autour\nd'une coordonnée GPS (restaurant, café, hôtel, musée, château,\néglise, plage, gare, etc. — 60+ kinds). Source : Overpass API\n(OpenStreetMap, free, no key). Cache mem 24h + disk forever\npar (lat5, lon5, radius). Scoring kind-aware : un restaurant\ngagne contre un shop générique à distance égale. Tags étendus\n(cuisine, religion, museum:type, brand, country) pour des\nlibellés narratifs riches. STRONG_POI_KINDS exposé pour les\nheuristiques métier.\n","keywords":["poi","osm","openstreetmap","overpass","geocoding","place","nearest","cache","server-side","restaurant","cafe","museum","landmark"],"use_when":["Tu veux nommer le POI le plus proche d'une coord (live tracking, photo geotag, etc.)","Géo-narration : 'à 50m de la Cathédrale Saint-Front'","Détection de stops dans un GPX (couple avec trace-smoother)"],"avoid_when":["Tu n'as pas de runtime serveur (browser uniquement) — Overpass refuse CORS","Tu veux des POI hors-OSM (utiliser Foursquare/Google Places — paid)"],"repo":"github.com/ateliersam86/atelier-resources","repo_subpath":"packages/poiLookup.ts","local_paths":["~/.claude/skills/atelier-resources/packages/poiLookup.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/server/poiLookup.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-resources/packages/poiLookup.ts $PROJECT_ROOT/server/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-resources/main/packages/poiLookup.ts \\\n  -o server/poiLookup.ts\n"},"requirements":[{"id":"cache_dir","title":"Dossier de cache disk inscriptible","env_var":"POI_LOOKUP_CACHE_DIR","default":"./.cache/poi-lookup","check":"[ -w \"${POI_LOOKUP_CACHE_DIR:-.cache/poi-lookup}\" ] || mkdir -p \"${POI_LOOKUP_CACHE_DIR:-.cache/poi-lookup}\"","install":"mkdir -p \"${POI_LOOKUP_CACHE_DIR:-.cache/poi-lookup}\"\n"}],"usage_example":"import { findNearestPoi, STRONG_POI_KINDS } from \"@/server/poiLookup\";\n\n// Trouve le POI le plus pertinent dans 80m\nconst { poi, source } = await findNearestPoi(48.8566, 2.3522);\nif (poi) {\n  console.log(`${poi.name} (${poi.kind}, ${poi.distanceM}m)`);\n  if (poi.cuisine) console.log(`  cuisine: ${poi.cuisine}`);\n}\n// source: \"cache\" | \"disk\" | \"live\"\n\n// Test si un POI mérite un seuil stationary plus court\nif (poi && STRONG_POI_KINDS.has(poi.kind)) {\n  // user a probablement vraiment fait halte\n}\n","api_surface":["findNearestPoi(lat, lon, radius=80) → Promise<{ poi: PoiRecord | null, source }>","STRONG_POI_KINDS: ReadonlySet<PoiKind>","type PoiKind (60+ values), PoiRecord, PoiSource"],"added":"2026-05-12","last_validated":"2026-05-12"}],"mcp_servers":[{"name":"hub-atelier","title":"MCP Hub Atelier de Sam","status":"stable","description":"Façade MCP au-dessus de l'API REST du Hub Atelier de Sam. Permet\nà Claude Code et aux agents externes de paramétrer le Hub\n(règles de scoring/parsing, prompts LLM), de le diagnostiquer\n(catalogue, erreurs de scraping, journal d'audit) et de déléguer\nà l'agent IA interne du site — sans toucher au code source.\nToutes les mutations sont audit-loggées et rollbackables.\n","transport":"streamable-http","endpoint":"http://192.168.1.72:5182/mcp","network":"LAN-only (jamais exposé publiquement)","auth":"Bearer ${HUB_MCP_TOKEN}","tools":["hub_health — état RAM/CPU/disque/Redis du Hub","hub_search_products — recherche catalogue","hub_catalog_stats — stats globales du catalogue","hub_scrape_errors — erreurs de scraping récentes","hub_audit_recent — journal d'audit des mutations","hub_list_rules — règles modifiables (scoring/parsing/prompts)","hub_update_scoring_weight — modifie un poids de ranking","hub_update_parsing_rule — modifie une règle de parsing","hub_update_prompt — modifie un prompt LLM","hub_rollback_audit — annule une mutation via l'audit_log","hub_ask_agent — délègue une question à l'agent interne du Hub"],"consumers":["claude-code"],"keywords":["hub","atelier","scoring","parsing","prompts","audit","rollback","catalogue","scraping"],"use_when":["Paramétrer les règles du Hub sans scp + docker restart","Diagnostiquer le Hub (erreurs scraping, audit) sans SSH","Déléguer une question catalogue à l'agent interne du site"],"code":"repo hub-atelier-de-sam — mcp_server/ ; service Docker `mcp` ; config .mcp.json","repo":"github.com/ateliersam86/hub-atelier-de-sam","added":"2026-05-16T00:00:00.000Z","last_validated":"2026-05-16T00:00:00.000Z"},{"name":"atelier-resources","title":"MCP atelier-resources (ce catalogue)","status":"stable","description":"Sert le catalogue des ressources réutilisables de l'atelier\n(librairies de code ET, à terme, serveurs MCP) à n'importe quel\nagent supportant MCP. Hot-reload du YAML à chaque requête.\nC'est le MCP de ce skill — méta : il s'auto-catalogue.\n","transport":"stdio + streamable-http","endpoint":"stdio (local) ; http container atelier-resources-mcp:3016 ; public resources.atelier-sam.fr/mcp","network":"stdio local + HTTP public (catalogue en lecture seule)","auth":"aucune (catalogue public read-only)","tools":["list_libraries — liste les libs du catalogue, filtres optionnels","search_libraries — recherche par mot-clé","get_library — détails complets d'une lib (install, usage, API)"],"consumers":["claude-code","cursor","codex-cli","openclaw"],"keywords":["catalogue","librairies","reusable","atelier","skill"],"use_when":["Découvrir les primitives réutilisables avant d'écrire du code","Vérifier qu'une lib existante résout déjà le problème"],"code":"skill ~/.claude/skills/atelier-resources/ — mcp-server/","repo":"github.com/ateliersam86/atelier-resources","added":"2026-05-12T00:00:00.000Z","last_validated":"2026-05-16T00:00:00.000Z"}],"generatedAt":"2026-05-16T16:50:47.894Z"}