[{"data":1,"prerenderedAt":1247},["ShallowReactive",2],{"profil-header-en":3,"profil-footer-en":93,"i-lucide:menu":115,"i-simple-icons:github":120,"i-simple-icons:linkedin":122,"i-lucide:mail":125,"projet-courant-en-\u002Fprojets\u002Fcatan":127,"projets-navigation-en":216,"i-lucide:arrow-right":1245},{"id":4,"accroche":5,"apropos":6,"competences":11,"cv":71,"email":72,"extension":73,"localisation":74,"meta":75,"nom":76,"reseaux":77,"stem":90,"titre":91,"__hash__":92},"profil_en\u002Fen\u002Fprofil.yml","I design and build web applications end to end — from the database to the pixel — and the AI tooling that plugs into them. Vue and Nuxt on the interface, Python and Node on the server, Docker and a VPS to ship it.",[7,8,9,10],"I'm a full-stack developer, and I build whole products rather than pieces of products: data model, API, interface, deployment and monitoring. That wide view is what interests me — understanding how a schema decision ripples all the way out to the user experience.","My personal projects tend to revolve around geographic data and real time: mapping wildfires from satellite feeds, merging public real-estate sources, tracking satellites in orbit. I like subjects where the raw data is thankless and the work consists of making it readable.","I work in Docker end to end, I host on my own servers, and I document what I build — because a project you can't pick back up six months later isn't really finished.","I didn't come to development through computing, but through objects: industrial product design first, then real-time 3D. That's where I picked up a taste for hard constraints and geometry, and the habit of starting from a brief rather than from a technology. Code showed up as the most direct way to automate whatever was slowing me down — and never left.",[12,21,30,39,47,55,62],{"categorie":13,"icon":14,"items":15},"Front-end","lucide:layout-dashboard",[16,17,18,19,20],"Vue 3","Nuxt","TypeScript","Tailwind CSS","MapLibre GL",{"categorie":22,"icon":23,"items":24},"Back-end","lucide:server",[25,26,27,28,29],"Python","FastAPI","Node.js","AdonisJS","PHP",{"categorie":31,"icon":32,"items":33},"Data","lucide:database",[34,35,36,37,38],"PostgreSQL","PostGIS","MongoDB","Redis","SQLite",{"categorie":40,"icon":41,"items":42},"AI & Data science","lucide:brain-circuit",[43,44,45,46],"TensorFlow","Keras","Vertex AI","pandas",{"categorie":48,"icon":49,"items":50},"Infrastructure","lucide:container",[51,52,53,54],"Docker","nginx","Caddy","GitHub Actions",{"categorie":56,"icon":57,"items":58},"Mobile","lucide:smartphone",[59,60,61],"SwiftUI","SwiftData","iOS",{"categorie":63,"icon":64,"items":65},"Method & product","lucide:git-branch",[66,67,68,69,70],"GitHub Flow","Testing","Documentation","Product Ownership","Agile","","rs.szmygiel@gmail.com","yml","Bordeaux, France",{},"Ronan Szmygiel",[78,82,86],{"nom":79,"url":80,"icon":81},"GitHub","https:\u002F\u002Fgithub.com\u002Fnanro22","simple-icons:github",{"nom":83,"url":84,"icon":85},"LinkedIn","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fszmygiel-ronan\u002F","simple-icons:linkedin",{"nom":87,"url":88,"icon":89},"Email","mailto:rs.szmygiel@gmail.com","lucide:mail","en\u002Fprofil","AI & Web Developer \u002F Product Owner","ZIzTObRSPg6iBvcjNxFpN9tVpN-w_PoqFkwZVJ9VFb8",{"id":4,"accroche":5,"apropos":94,"competences":95,"cv":71,"email":72,"extension":73,"localisation":74,"meta":110,"nom":76,"reseaux":111,"stem":90,"titre":91,"__hash__":92},[7,8,9,10],[96,98,100,102,104,106,108],{"categorie":13,"icon":14,"items":97},[16,17,18,19,20],{"categorie":22,"icon":23,"items":99},[25,26,27,28,29],{"categorie":31,"icon":32,"items":101},[34,35,36,37,38],{"categorie":40,"icon":41,"items":103},[43,44,45,46],{"categorie":48,"icon":49,"items":105},[51,52,53,54],{"categorie":56,"icon":57,"items":107},[59,60,61],{"categorie":63,"icon":64,"items":109},[66,67,68,69,70],{},[112,113,114],{"nom":79,"url":80,"icon":81},{"nom":83,"url":84,"icon":85},{"nom":87,"url":88,"icon":89},{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":119},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"\u002F>",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":121},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":123,"hidden":124},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",true,{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":126},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"\u002F>\u003Crect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"\u002F>\u003C\u002Fg>",{"id":128,"title":129,"architecture":130,"body":143,"cover":71,"description":196,"extension":197,"featured":118,"gallery":198,"highlights":199,"interface":124,"meta":203,"navigation":124,"path":204,"rang":205,"repo":71,"role":206,"seo":207,"site":208,"status":209,"stem":210,"tags":211,"type":213,"year":214,"__hash__":215},"projets_en\u002Fprojets\u002Fcatan.md","Catan",[131,134,137,140],{"label":132,"description":133},"State","Game state lives entirely on the server, never in the client.",{"label":135,"description":136},"Validation","Every action is checked server-side — turn, resources, legality — before it is applied.",{"label":138,"description":139},"Broadcast","The new state is pushed back to every participant over WebSocket.",{"label":141,"description":142},"Persistence","Games and move history kept in PostgreSQL, environment started with a single docker compose command.",{"type":144,"value":145,"toc":190},"minimark",[146,151,155,159,166,169,172,176,187],[147,148,150],"h2",{"id":149},"the-project","The project",[152,153,154],"p",{},"A digital version of Catan, playable multiplayer in the browser: randomly\ngenerated board, settlement and road placement, resource production, trading\nbetween players, the robber and victory point counting.",[147,156,158],{"id":157},"architecture","Architecture",[152,160,161,165],{},[162,163,164],"strong",{},"Stack",": Vue 3 · FastAPI · PostgreSQL · WebSocket",[152,167,168],{},"Game state lives on the server, never in the client. Every action a player sends\nis validated server-side — is it their turn, do they have the resources, is the\nspot legal — before it is applied and pushed back to every participant.",[152,170,171],{},"It's the only defensible architecture for a multiplayer game: a client that\nworks out for itself what it's allowed to do is a client you can tamper with.",[147,173,175],{"id":174},"what-the-game-teaches","What the game teaches",[152,177,178,179,182,183,186],{},"Catan is an excellent modelling exercise. The board is a hexagonal tiling where\nsettlements sit on ",[162,180,181],{},"vertices"," and roads on ",[162,184,185],{},"edges"," — so three coordinate\nsystems coexist, and half the work is moving cleanly between them: which tiles\ntouch this vertex, which edges start here, which vertices are too close to take\na new settlement.",[152,188,189],{},"Once that geometry is laid down correctly, the game rules become almost trivial\nto implement. Laid down crooked, every rule becomes a special case.",{"title":71,"searchDepth":191,"depth":191,"links":192},2,[193,194,195],{"id":149,"depth":191,"text":150},{"id":157,"depth":191,"text":158},{"id":174,"depth":191,"text":175},"A multiplayer web adaptation of the board game Catan, playable together in a browser in real time.","md",[],[200,201,202],"Multiplayer games synchronised in real time over WebSocket","Game rules and move validation entirely server-side","Full stack up and running with a single docker compose command",{},"\u002Fprojets\u002Fcatan",5,"Full-stack design and development",{"title":129,"description":196},"https:\u002F\u002Fcatan.szmygiel.com","Live","projets\u002Fcatan",[16,26,25,34,212,51],"WebSocket","Web",2025,"uC6d3QfDReLfb9V3QMHz_nksiLKIELYYau75-kASmt4",[217,259,404,532,705,810,875,1022],{"id":128,"title":129,"architecture":218,"body":223,"cover":71,"description":196,"extension":197,"featured":118,"gallery":254,"highlights":255,"interface":124,"meta":256,"navigation":124,"path":204,"rang":205,"repo":71,"role":206,"seo":257,"site":208,"status":209,"stem":210,"tags":258,"type":213,"year":214,"__hash__":215},[219,220,221,222],{"label":132,"description":133},{"label":135,"description":136},{"label":138,"description":139},{"label":141,"description":142},{"type":144,"value":224,"toc":249},[225,227,229,231,235,237,239,241,247],[147,226,150],{"id":149},[152,228,154],{},[147,230,158],{"id":157},[152,232,233,165],{},[162,234,164],{},[152,236,168],{},[152,238,171],{},[147,240,175],{"id":174},[152,242,178,243,182,245,186],{},[162,244,181],{},[162,246,185],{},[152,248,189],{},{"title":71,"searchDepth":191,"depth":191,"links":250},[251,252,253],{"id":149,"depth":191,"text":150},{"id":157,"depth":191,"text":158},{"id":174,"depth":191,"text":175},[],[200,201,202],{},{"title":129,"description":196},[16,26,25,34,212,51],{"id":260,"title":261,"architecture":262,"body":274,"cover":377,"description":378,"extension":197,"featured":124,"gallery":379,"highlights":389,"interface":124,"meta":393,"navigation":124,"path":394,"rang":395,"repo":71,"role":206,"seo":396,"site":71,"status":397,"stem":398,"tags":399,"type":213,"year":214,"__hash__":403},"projets_en\u002Fprojets\u002Festimap.md","Estimap",[263,266,269,271],{"label":264,"description":265},"Frontend","Nuxt 3 + MapLibre GL (PMTiles) — map, property sheet, back office.",{"label":267,"description":268},"Backend","FastAPI + SQLAlchemy, Celery for long-running work, two separate queues (imports\u002Fmutations).",{"label":31,"description":270},"PostgreSQL \u002F PostGIS (~100M rows) for the spatial layer, Redis as broker and cache.",{"label":272,"description":273},"Infra","Docker Compose, workers split by queue.",{"type":144,"value":275,"toc":369},[276,280,283,286,290,293,303,305,329,341,348,352,355,359,362,366],[147,277,279],{"id":278},"the-problem","The problem",[152,281,282],{},"French real-estate data is public and free. It is also scattered: transactions\nsit in the DVF sales register, parcel geometry in the cadastre, building\ncharacteristics in the BDNB registry, energy performance in the DPE dataset,\ndemographics at INSEE, hazard exposure somewhere else again — and plenty more:\nlocal amenities, transport, fibre coverage, schools, building permits,\ncondominiums.",[152,284,285],{},"Each has its own format, its own granularity and its own pivot identifier.\nCross-referencing \"the price per square metre of houses built before 1975 with\nan F or G energy rating in a flood-prone area\" therefore takes a considerable\namount of upfront merging.",[147,287,289],{"id":288},"the-product-goal","The product goal",[152,291,292],{},"A property price estimation engine — the map is the exploration surface, but the\npoint is to feed a model with genuinely cross-referenced features.",[152,294,295,298,299,302],{},[162,296,297],{},"Phase 1"," (weighted kNN comparables, progressive radii, plausibility guards)\nis in production and answers 100% of estimation requests today. ",[162,300,301],{},"Phase 2","\n(LightGBM, 27 features) is in final training before promotion, behind an\nautomatic gate that compares any new model against the one currently served.",[147,304,158],{"id":157},[306,307,308,314,319,324],"ul",{},[309,310,311,313],"li",{},[162,312,264],{},": Nuxt 3 + MapLibre GL, PMTiles — map, property sheet, back office",[309,315,316,318],{},[162,317,267],{},": FastAPI + SQLAlchemy, with Celery for long-running work",[309,320,321,323],{},[162,322,31],{},": PostgreSQL \u002F PostGIS for the spatial layer, Redis as broker and cache",[309,325,326,328],{},[162,327,272],{},": docker-compose, workers split by queue",[152,330,331,332,336,337,340],{},"Workers are split across two queues: ",[333,334,335],"code",{},"imports"," for source ingestion, and\n",[333,338,339],{},"mutations"," for rebuilds, enrichment, tile generation and model training. That\nseparation keeps a multi-hour import from blocking a tile rebuild that takes a\nfew minutes.",[152,342,343,344,347],{},"An ",[333,345,346],{},"\u002Fadmin"," back office drives 15 source connectors and tracks import state.",[147,349,351],{"id":350},"what-the-map-covers","What the map covers",[152,353,354],{},"Around 7 million property sales (18M raw rows in the sales register) shown as\npoints coloured by price per square metre or as a heatmap, filterable by type,\nprice, energy rating and pool. A click opens the property sheet: sale history,\nextended energy rating, building context (outbuildings, fibre, school with its\nsocial index, condominium, recent permits, parks, water, noise), live regulatory\nhazards, and nearby amenities that can be layered onto the map.",[147,356,358],{"id":357},"method","Method",[152,360,361],{},"The project rests on documentation in ten files — architecture, sources, data\nmodel, pipelines, API, frontend, back office, runbook, roadmap — kept current\nwith every significant change. The runbook in particular records the\ninfrastructure traps learned the hard way, which saves relearning them.",[147,363,365],{"id":364},"what-i-took-away","What I took away",[152,367,368],{},"On this kind of project the difficulty is never the algorithm: it's reconciling\nidentifiers across registries that were never designed to talk to each other. A\ncadastral parcel and a building record share no key — it has to be rebuilt\nspatially, and you have to accept an imperfect match rate. And a quality metric\n(a backtest median absolute percentage error, say) is only worth what its\nprotocol is worth: a temporal leak between comparables and the tested sale\nskewed the headline number for a long time.",{"title":71,"searchDepth":191,"depth":191,"links":370},[371,372,373,374,375,376],{"id":278,"depth":191,"text":279},{"id":288,"depth":191,"text":289},{"id":157,"depth":191,"text":158},{"id":350,"depth":191,"text":351},{"id":357,"depth":191,"text":358},{"id":364,"depth":191,"text":365},"\u002Fprojets\u002Festimap-carte.png","An interactive map of France merging 16 public real-estate data sources — sales records, cadastre, building registry, energy ratings, census, hazards, schools, transport — behind a price estimation engine.",[380,383,386],{"url":381,"caption":382},"\u002Fprojets\u002Festimap-fiche-bien.png","Property sheet — sale, energy rating, official hazard exposure, context and nearby amenities.",{"url":384,"caption":385},"\u002Fprojets\u002Festimap-tableau-de-bord.png","Back office — service health, sales tiles and live job tracking.",{"url":387,"caption":388},"\u002Fprojets\u002Festimap-sources.png","Back office — driving the 15 data-source connectors.",[390,391,392],"16 public data sources merged across ~7 million sales","Estimation engine in production (kNN comparables, LightGBM in the wings)","A 14-step enrichment pipeline driven from a back office",{},"\u002Fprojets\u002Festimap",3,{"title":261,"description":378},"Completed","projets\u002Festimap",[400,20,26,401,35,37,402],"Nuxt 3","Celery","LightGBM","r1KLUD8K6yLxger7FFaaA_6RvAUCkR2_Jt1TMYJEcQM",{"id":405,"title":406,"architecture":407,"body":420,"cover":514,"description":515,"extension":197,"featured":118,"gallery":516,"highlights":517,"interface":118,"meta":521,"navigation":124,"path":522,"rang":523,"repo":71,"role":206,"seo":524,"site":525,"status":209,"stem":526,"tags":527,"type":213,"year":530,"__hash__":531},"projets_en\u002Fprojets\u002Fglobe-satellites.md","Globe Satellites",[408,411,414,417],{"label":409,"description":410},"Sources","TLEs refreshed every 2h from Celestrak, with automatic failover to a backup proxy when the network blocks them.",{"label":412,"description":413},"Compute","Vectorised SGP4 propagation (SatrecArray + NumPy) with Skyfield — every satellite recomputed in a single call every 60 seconds.",{"label":415,"description":416},"Delivery","One shared computation broadcast over WebSocket to every connected client, in parallel.",{"label":418,"description":419},"Rendering","3D globe (Globe.gl \u002F Three.js), a single InstancedMesh for all satellites, ARM64 Docker image on a Raspberry Pi 4.",{"type":144,"value":421,"toc":507},[422,424,427,430,434,460,464,475,479,492,496],[147,423,150],{"id":149},[152,425,426],{},"Several thousand active satellites circle above our heads, and their orbits are\npublic. Celestrak publishes orbital elements in the TLE format (Two-Line\nElement), which is enough to compute the position of any object at any instant.",[152,428,429],{},"Globe Satellites shows that ballet on a 3D globe, refreshed every sixty seconds.",[147,431,433],{"id":432},"features","Features",[306,435,436,442,448,454],{},[309,437,438,441],{},[162,439,440],{},"Filtering by category"," — starlink, oneweb, communications, navigation,\nstations, weather, Earth observation, science, other — clickable in the HUD,\nwith \"Show all\" \u002F \"Hide all\" buttons",[309,443,444,447],{},[162,445,446],{},"Hover tooltip"," — name, altitude and category for each satellite",[309,449,450,453],{},[162,451,452],{},"Logarithmic radial scale"," — visibly separates low, medium and\ngeostationary orbits instead of stacking them on top of each other",[309,455,456,459],{},[162,457,458],{},"Automatic WebSocket reconnection"," (exponential backoff), with a watchdog\nthat catches a frozen connection with no clean TCP close",[147,461,463],{"id":462},"computing-on-the-server","Computing on the server",[152,465,466,467,470,471,474],{},"The structural choice is to ",[162,468,469],{},"compute server-side",". SGP4 orbital propagation\nfor several thousand objects, once a minute, is not work for the browser — all\nthe more so since every connected client shares exactly the same result. The\nserver computes once (",[333,472,473],{},"SatrecArray"," + NumPy, a single vectorised call rather\nthan a Python loop — roughly 24x faster), broadcasts to everyone over WebSocket\nin parallel, and the client only renders.",[147,476,478],{"id":477},"network-resilience","Network resilience",[152,480,481,482,485,486,491],{},"TLEs lose accuracy as the days pass — propagating from week-old elements drifts\nnoticeably — hence an automatic refresh every two hours. Loading follows a\nthree-tier chain: the full Celestrak group first, a fallback to 37 groups\ndownloaded in parallel if that fails or returns a 403, then a backup proxy if\nboth fail — a sign of a network block rather than an isolated incident. It\nreturns to the direct route on its own, with no manual action. Data freshness is\nexposed through a ",[333,483,484],{},"\u002Fhealth"," endpoint, which pushes best-effort\n",[487,488,490],"a",{"href":489},"\u002Fen\u002Fprojects\u002Fvigie","Vigie"," monitoring for the refresh cycle.",[147,493,495],{"id":494},"the-hardware-constraint","The hardware constraint",[152,497,498,499,502,503,506],{},"The project is built for a ",[162,500,501],{},"Raspberry Pi 4 on ARM64",". That constraint drove\nseveral decisions: a multi-architecture Docker image, vectorised NumPy\ncomputation rather than a Python loop, a single Three.js ",[333,504,505],{},"InstancedMesh"," rather\nthan one object per satellite, and broadcasting one shared state rather than\ncomputing per connected client.",{"title":71,"searchDepth":191,"depth":191,"links":508},[509,510,511,512,513],{"id":149,"depth":191,"text":150},{"id":432,"depth":191,"text":433},{"id":462,"depth":191,"text":463},{"id":477,"depth":191,"text":478},{"id":494,"depth":191,"text":495},"\u002Fprojets\u002Fglobe-satellites-carte.png","An interactive 3D globe showing the live position of active satellites in Earth orbit, built to run on a Raspberry Pi.",[],[518,519,520],"Positions recomputed every 60 seconds, vectorised SGP4 propagation (~24x faster than a loop)","Automatic failover to a backup proxy if Celestrak becomes unreachable","Every satellite rendered in a single draw call (Three.js InstancedMesh)",{},"\u002Fprojets\u002Fglobe-satellites",4,{"title":406,"description":515},"https:\u002F\u002Fglobe.szmygiel.com","projets\u002Fglobe-satellites",[16,528,26,25,212,51,529],"Globe.gl","Raspberry Pi",2026,"TS8Sx0gdo3anp3sYPh3mls3nfLiVHR3E-exVBbiOAZM",{"id":533,"title":534,"architecture":535,"body":546,"cover":684,"description":685,"extension":197,"featured":124,"gallery":686,"highlights":693,"interface":124,"meta":697,"navigation":124,"path":698,"rang":191,"repo":71,"role":206,"seo":699,"site":700,"status":209,"stem":701,"tags":702,"type":213,"year":530,"__hash__":704},"projets_en\u002Fprojets\u002Fpyro-radar.md","Pyro Radar",[536,538,541,544],{"label":409,"description":537},"Six satellite feeds aggregated — NASA FIRMS, GOES-18\u002F19, Meteosat MTG, Himawari-9, Sentinel-3.",{"label":539,"description":540},"Ingestion","One ingester per source (CSV, NetCDF, HDF5) normalises into a shared schema of geolocated detections.",{"label":542,"description":543},"Storage","Burned areas estimated as polygons directly in PostGIS, in batches.",{"label":418,"description":545},"MapLibre GL vector tiles, real-time delivery over Server-Sent Events.",{"type":144,"value":547,"toc":678},[548,552,555,558,562,636,639,643,649,655,661,667,673,675],[147,549,551],{"id":550},"the-idea","The idea",[152,553,554],{},"Fire detection data is public, free and abundant — NASA, ESA, NOAA and JAXA\nbroadcast it continuously. It also comes in heterogeneous formats, at different\ncadences, with geographic coverage that only partly overlaps.",[152,556,557],{},"Pyro Radar merges those feeds into a single worldwide map, refreshed continuously.",[147,559,561],{"id":560},"the-sources","The sources",[563,564,565,581],"table",{},[566,567,568],"thead",{},[569,570,571,575,578],"tr",{},[572,573,574],"th",{},"Source",[572,576,577],{},"Coverage",[572,579,580],{},"Cadence",[582,583,584,596,607,617,627],"tbody",{},[569,585,586,590,593],{},[587,588,589],"td",{},"NASA FIRMS (VIIRS \u002F MODIS)",[587,591,592],{},"Worldwide",[587,594,595],{},"Orbital passes",[569,597,598,601,604],{},[587,599,600],{},"GOES-18 \u002F 19",[587,602,603],{},"Americas",[587,605,606],{},"10 min",[569,608,609,612,615],{},[587,610,611],{},"Meteosat MTG (LSA SAF)",[587,613,614],{},"Europe \u002F Africa",[587,616,606],{},[569,618,619,622,625],{},[587,620,621],{},"Himawari-9",[587,623,624],{},"Asia \u002F Oceania",[587,626,606],{},[569,628,629,632,634],{},[587,630,631],{},"Sentinel-3 SLSTR (Copernicus)",[587,633,592],{},[587,635,595],{},[152,637,638],{},"Geostationary satellites bring the frequency, polar orbits bring the resolution.",[147,640,642],{"id":641},"the-technical-work","The technical work",[152,644,645,648],{},[162,646,647],{},"Ingestion."," CSV for FIRMS, NetCDF for GOES, HDF5 for MTG: one ingester per\nsource normalises into a shared schema of geolocated detections.",[152,650,651,654],{},[162,652,653],{},"Burned areas."," Point detections become estimated polygons directly in\nPostGIS, in batches — loading every zone at once was taking Postgres down.",[152,656,657,660],{},[162,658,659],{},"Wind."," GFS data is only decoded around active fires. The worldwide grid\nwould have weighed several gigabytes per cycle for information that only matters\nover a few dozen square kilometres.",[152,662,663,666],{},[162,664,665],{},"Real time."," Server-Sent Events: no polling, and no bidirectional WebSocket\nwhere a one-way stream is enough.",[152,668,669,672],{},[162,670,671],{},"Mapping."," MapLibre GL, OpenFreeMap basemap and Esri satellite view — no API\nkey, so no quota and no surprise bill.",[147,674,365],{"id":364},[152,676,677],{},"Adding a coverage area is one line of SQL. That design constraint, set early,\nkept the geographic logic from scattering across the application code.",{"title":71,"searchDepth":191,"depth":191,"links":679},[680,681,682,683],{"id":550,"depth":191,"text":551},{"id":560,"depth":191,"text":561},{"id":641,"depth":191,"text":642},{"id":364,"depth":191,"text":365},"\u002Fprojets\u002Fpyro-radar-carte.png","Real-time wildfire tracking worldwide, built from six satellite sources aggregated onto a single vector map.",[687,690],{"url":688,"caption":689},"\u002Fprojets\u002Fpyro-radar-points-chauds.png","Hotspots — individual detections with confidence and per-click detail.",{"url":691,"caption":692},"\u002Fprojets\u002Fpyro-radar-historique.png","30-day history — cumulative fire count and estimated burned area.",[694,695,696],"Six satellite sources aggregated, from polar orbits to geostationary","Burned-area estimates computed directly in PostGIS","Real-time delivery over Server-Sent Events, no polling",{},"\u002Fprojets\u002Fpyro-radar",{"title":534,"description":685},"https:\u002F\u002Fpyroradar.com","projets\u002Fpyro-radar",[16,20,28,35,34,703],"SSE","_v_G5VhIz3MViZGbQOxvjVqVhYkR5cKTu99Y43u2FR8",{"id":706,"title":707,"architecture":708,"body":720,"cover":71,"description":794,"extension":197,"featured":118,"gallery":795,"highlights":796,"interface":124,"meta":800,"navigation":124,"path":801,"rang":802,"repo":71,"role":803,"seo":804,"site":71,"status":397,"stem":805,"tags":806,"type":56,"year":530,"__hash__":809},"projets_en\u002Fprojets\u002Frootine.md","Rootine",[709,711,714,717],{"label":141,"description":710},"Habits and check-offs stored locally with SwiftData, with no external dependency.",{"label":712,"description":713},"Check-off","A Yesterday \u002F Today toggle to check off retroactively, without breaking streaks.",{"label":715,"description":716},"Visualisation","Monthly heatmap calendar where colour intensity reflects the completion rate.",{"label":718,"description":719},"Interface","Spring animations and haptic feedback on every check, in SwiftUI.",{"type":144,"value":721,"toc":788},[722,728,730,746,748,754,760,766,772,776,779,782,785],[723,724,725],"blockquote",{},[152,726,727],{},"Habits are the roots of who you become.",[147,729,150],{"id":149},[152,731,732,733,737,738,741,742,745],{},"Rootine — a play on ",[734,735,736],"em",{},"root"," and ",[734,739,740],{},"routine"," — is a habit tracker built around one\nsimple principle: ",[162,743,744],{},"consistency beats intensity",". Ten minutes of exercise every\nday is worth more than one two-hour session a week.",[147,747,433],{"id":432},[152,749,750,753],{},[162,751,752],{},"Home"," — the checklist of today's habits, with an animated checkbox (spring\nand haptic feedback), an overall progress bar and a congratulations message when\neverything is done. A Yesterday \u002F Today toggle allows checking off retroactively.",[152,755,756,759],{},[162,757,758],{},"Routines"," — creating, editing and deleting the habits you track.",[152,761,762,765],{},[162,763,764],{},"Calendar"," — a monthly heatmap where colour intensity reflects that day's\ncompletion rate. Consistency becomes visible at a glance.",[152,767,768,771],{},[162,769,770],{},"Statistics"," — current streaks, completion rate, trends over time.",[147,773,775],{"id":774},"the-stance","The stance",[152,777,778],{},"Most habit apps punish forgetting: broken streaks, lost badges, guilt-tripping\nnotifications. That lever motivates for a week, then produces the opposite\neffect — you uninstall rather than face the dashboard.",[152,780,781],{},"Rootine takes the opposite stance. You can check off yesterday, no streak breaks\nfor good, and the tone stays encouraging. The point is to keep going for months,\nnot to win a score.",[147,783,164],{"id":784},"stack",[152,786,787],{},"SwiftUI and SwiftData, with no external dependency. All data stays on the device.",{"title":71,"searchDepth":191,"depth":191,"links":789},[790,791,792,793],{"id":149,"depth":191,"text":150},{"id":432,"depth":191,"text":433},{"id":774,"depth":191,"text":775},{"id":784,"depth":191,"text":164},"A minimal, forgiving iOS habit tracker — today's checklist, a heatmap calendar and consistency statistics.",[],[797,798,799],"Retroactive check-off for yesterday, without the guilt trip","Monthly heatmap calendar and trend statistics","Spring animations and haptic feedback on every check",{},"\u002Fprojets\u002Frootine",8,"iOS design and development",{"title":707,"description":794},"projets\u002Frootine",[59,60,61,807,808],"Swift","Charts","-jczwI2MfId-BcdlMwAxDkg111FvQlpAM7nKRbRdCv8",{"id":811,"title":812,"architecture":813,"body":824,"cover":71,"description":861,"extension":197,"featured":118,"gallery":862,"highlights":863,"interface":124,"meta":867,"navigation":124,"path":868,"rang":869,"repo":71,"role":803,"seo":870,"site":71,"status":397,"stem":871,"tags":872,"type":56,"year":530,"__hash__":874},"projets_en\u002Fprojets\u002Fsofa.md","Sofa",[814,817,819,822],{"label":815,"description":816},"Search","TMDB API queries for title search and detail sheets.",{"label":542,"description":818},"Library and watch history persisted with SwiftData, readable offline.",{"label":820,"description":821},"Enrichment","Available streaming platforms pulled from TMDB.",{"label":718,"description":823},"Dark design system inspired by cinema auditoriums, in native SwiftUI.",{"type":144,"value":825,"toc":855},[826,828,831,833,840,843,847,850,852],[147,827,150],{"id":149},[152,829,830],{},"Sofa answers an everyday problem: knowing what to watch, and above all\nremembering what you meant to watch. The app lets you search for a film or\nseries through the TMDB API, add it to your library, mark what you've seen, rate\ntitles and see where to stream them.",[147,832,164],{"id":784},[152,834,835,836,839],{},"A ",[162,837,838],{},"100% native iOS"," app, written in SwiftUI with SwiftData for persistence. No\ncross-platform framework, no WebView: the animations, the gestures and the\nsystem behaviour are those of an iOS app, because that is precisely what\nseparates an app you keep from an app you delete.",[152,841,842],{},"SwiftData handles local persistence, which makes the library readable offline —\nonly search and detail refreshes need the network.",[147,844,846],{"id":845},"design","Design",[152,848,849],{},"A dark interface, inspired by the atmosphere of a cinema: deep backgrounds,\nposters given room to breathe, high-contrast typography. The design system is\ndefined once — colours, spacing, type — then applied consistently across every\nscreen.",[147,851,365],{"id":364},[152,853,854],{},"Coming from the web, SwiftUI asks you to unlearn a few reflexes. The layout\nsystem doesn't work like Flexbox, and state propagation follows its own rules.\nIn exchange, the integration with the system — widgets, sharing, dark mode,\naccessibility — is on a different level from anything you get in a web app.",{"title":71,"searchDepth":191,"depth":191,"links":856},[857,858,859,860],{"id":149,"depth":191,"text":150},{"id":784,"depth":191,"text":164},{"id":845,"depth":191,"text":846},{"id":364,"depth":191,"text":365},"A native iOS watchlist app for films and series — TMDB search, personal library, watch tracking and ratings.",[],[864,865,866],"Local persistence with SwiftData, usable offline","TMDB integration — search, detail sheets and streaming platforms","Dark design system inspired by cinema auditoriums",{},"\u002Fprojets\u002Fsofa",7,{"title":812,"description":861},"projets\u002Fsofa",[59,60,61,873,807],"TMDB API","L9BjW5Ev8RnMN90W7J3oAsl5_rNEZpLJ4xhveeWZqgw",{"id":876,"title":877,"architecture":878,"body":889,"cover":1003,"description":1004,"extension":197,"featured":118,"gallery":1005,"highlights":1006,"interface":118,"meta":1010,"navigation":124,"path":1011,"rang":1012,"repo":71,"role":1013,"seo":1014,"site":71,"status":397,"stem":1015,"tags":1016,"type":31,"year":214,"__hash__":1021},"projets_en\u002Fprojets\u002Fthanos.md","Thanos",[879,881,884,887],{"label":31,"description":880},"Data Manager — fetching and caching historical series per exchange and timeframe.",{"label":882,"description":883},"Indicators","Custom Indicators — technical indicators implemented by hand rather than imported.",{"label":885,"description":886},"Backtest","Backtest Analysis — performance and risk metrics across five strategy families.",{"label":715,"description":888},"Plot Analysis — entries, exits and equity curves.",{"type":144,"value":890,"toc":997},[891,893,896,900,954,957,961,987,991,994],[147,892,150],{"id":149},[152,894,895],{},"Thanos is a complete environment for prototyping, testing and comparing trading\nstrategies across several exchanges and timeframes. The goal isn't to trade:\nit's to have an honest test bed for the question \"would this idea have worked,\nand at what cost in risk\".",[147,897,899],{"id":898},"strategies-implemented","Strategies implemented",[563,901,902,912],{},[566,903,904],{},[569,905,906,909],{},[572,907,908],{},"Strategy",[572,910,911],{},"Principle",[582,913,914,922,930,938,946],{},[569,915,916,919],{},[587,917,918],{},"SMA",[587,920,921],{},"Simple moving average crossover",[569,923,924,927],{},[587,925,926],{},"EMA",[587,928,929],{},"Exponential moving average crossover",[569,931,932,935],{},[587,933,934],{},"Momentum (RSI)",[587,936,937],{},"Entries on oversold \u002F overbought zones",[569,939,940,943],{},[587,941,942],{},"SMA-EMA combined",[587,944,945],{},"Cross-confirmation between the two moving average families",[569,947,948,951],{},[587,949,950],{},"TRIX",[587,952,953],{},"Triple exponential smoothing oscillator",[152,955,956],{},"They all share the same interface, which makes it possible to evaluate them on\nidentical ground — same data, same fees, same metrics.",[147,958,960],{"id":959},"modules","Modules",[306,962,963,969,975,981],{},[309,964,965,968],{},[162,966,967],{},"Data Manager"," — fetching and caching historical series per exchange and timeframe",[309,970,971,974],{},[162,972,973],{},"Custom Indicators"," — technical indicators implemented by hand rather than imported",[309,976,977,980],{},[162,978,979],{},"Backtest Analysis"," — performance and risk metrics",[309,982,983,986],{},[162,984,985],{},"Plot Analysis"," — visualising entries, exits and equity curves",[147,988,990],{"id":989},"the-real-lesson","The real lesson",[152,992,993],{},"The hard part of a backtest isn't making it run: it's stopping it from lying.\nBiases slip in easily — using data that wasn't yet available at decision time,\nignoring fees and slippage, or tuning parameters on the same period used to\nvalidate the strategy.",[152,995,996],{},"A strategy that posts a spectacular backtest result is almost always the sign of\na data leak somewhere, not of a discovery.",{"title":71,"searchDepth":191,"depth":191,"links":998},[999,1000,1001,1002],{"id":149,"depth":191,"text":150},{"id":898,"depth":191,"text":899},{"id":959,"depth":191,"text":960},{"id":989,"depth":191,"text":990},"\u002Fprojets\u002Fthanos-cover.svg","A research, backtesting and analysis platform for trading strategies on cryptocurrency markets.",[],[1007,1008,1009],"Five strategy families comparable on a single test bed","Custom technical indicators implemented by hand","Historical data cached across multiple exchanges and timeframes",{},"\u002Fprojets\u002Fthanos",6,"Design and development",{"title":877,"description":1004},"projets\u002Fthanos",[25,1017,1018,1019,1020],"Pandas","NumPy","Backtesting","Data analysis","dSBxOYef84ONZdN6irpkgiOWjftugwl2o8W9AqvRZUY",{"id":1023,"title":490,"architecture":1024,"body":1035,"cover":1220,"description":1221,"extension":197,"featured":124,"gallery":1222,"highlights":1232,"interface":124,"meta":1237,"navigation":124,"path":1238,"rang":1239,"repo":71,"role":206,"seo":1240,"site":1241,"status":209,"stem":1242,"tags":1243,"type":213,"year":530,"__hash__":1244},"projets_en\u002Fprojets\u002Fvigie.md",[1025,1028,1030,1032],{"label":1026,"description":1027},"API","AdonisJS v6, TypeScript, Lucid — business logic and endpoints.",{"label":31,"description":1029},"PostgreSQL 17, progressive rollup (fine → hourly → daily) to keep retention affordable.",{"label":718,"description":1031},"Vue 3, Vite, Pinia — dashboard and public status page.",{"label":1033,"description":1034},"Exposure","Docker Compose on a VPS (Caddy, GitHub Actions), plus a read-only MCP server to query monitoring from the editor.",{"type":144,"value":1036,"toc":1212},[1037,1039,1042,1045,1049,1081,1084,1086,1133,1153,1157,1160,1174,1178,1185,1203,1205],[147,1038,279],{"id":278},[152,1040,1041],{},"I had a dozen projects online and no overall view: did last night's cron run? Is\nthe API still answering? How many visitors this week? Every answer meant logging\ninto a different server.",[152,1043,1044],{},"Plugging a third-party service into each project would have multiplied the\naccounts, the costs and the cookie banners. I preferred to build the single tool\nI was missing.",[147,1046,1048],{"id":1047},"what-vigie-does","What Vigie does",[306,1050,1051,1057,1063,1069,1075],{},[309,1052,1053,1056],{},[162,1054,1055],{},"Visit analytics"," with no cookie and no persistent identifier — no consent banner required",[309,1058,1059,1062],{},[162,1060,1061],{},"Uptime monitoring",": regular HTTP probes with history and uptime calculation",[309,1064,1065,1068],{},[162,1066,1067],{},"Scheduled-job supervision",": a cron that hasn't checked in within its window raises an alert",[309,1070,1071,1074],{},[162,1072,1073],{},"Error tracking"," for the application errors reported by the connected projects",[309,1076,1077,1080],{},[162,1078,1079],{},"A public status page",", fed by the same data as the internal dashboard",[152,1082,1083],{},"Every monitored project has its own API key, and never sees another project's data.",[147,1085,158],{"id":157},[563,1087,1088,1098],{},[566,1089,1090],{},[569,1091,1092,1095],{},[572,1093,1094],{},"Layer",[572,1096,1097],{},"Choice",[582,1099,1100,1107,1115,1122],{},[569,1101,1102,1104],{},[587,1103,1026],{},[587,1105,1106],{},"AdonisJS v6, TypeScript, Lucid",[569,1108,1109,1112],{},[587,1110,1111],{},"Database",[587,1113,1114],{},"PostgreSQL 17",[569,1116,1117,1119],{},[587,1118,718],{},[587,1120,1121],{},"Vue 3, Vite, Pinia, TypeScript",[569,1123,1124,1127],{},[587,1125,1126],{},"Deployment",[587,1128,1129,1130],{},"Docker Compose on a VPS, Caddy in front, GitHub Actions on ",[333,1131,1132],{},"main",[152,1134,1135,1136,1139,1140,1139,1143,1139,1146,1139,1149,1152],{},"Everything runs in containers — ",[333,1137,1138],{},"postgres",", ",[333,1141,1142],{},"redis",[333,1144,1145],{},"api",[333,1147,1148],{},"worker",[333,1150,1151],{},"web"," —\nwith no Node required on the host machine. The worker handles probes and\nrollups outside the request cycle, which keeps the API responsive no matter how\nmany projects are being monitored.",[147,1154,1156],{"id":1155},"the-client-side-agent","The client-side agent",[152,1158,1159],{},"Collecting means putting code inside someone else's page: a bug here doesn't\ndegrade Vigie, it degrades the site being monitored. So the script stays under\ntwo kilobytes, with no cookie and no browser fingerprint — and the budget is\nchecked at build time, so the build fails rather than shipping something heavier.",[152,1161,1162,1163,737,1166,1169,1170,1173],{},"It wraps ",[333,1164,1165],{},"pushState",[333,1167,1168],{},"replaceState"," to catch navigations in a single-page\napp, then restores the original methods: nothing is permanently altered for the\nhost application. A ",[333,1171,1172],{},"@vigie\u002Fjs"," plugin hooks straight into the Vue 3 router\ninstead of guessing from history events.",[147,1175,1177],{"id":1176},"an-mcp-server-to-stay-in-the-editor","An MCP server, to stay in the editor",[152,1179,1180,1181,1184],{},"What's broken? Is this error new? Did last night's cron run? Vigie exposes six\nread-only tools over ",[162,1182,1183],{},"MCP"," to answer those questions without opening the\ndashboard.",[152,1186,1187,1188,1191,1192,1139,1195,1198,1199,1202],{},"Two deliberate choices: ",[162,1189,1190],{},"no writes at all"," — an assistant that reads gets the\ndiagnosis wrong, an assistant that writes gets production wrong; and ",[162,1193,1194],{},"internal\ncodes translated on the way out",[333,1196,1197],{},"cause: \"silent agent\""," rather than\n",[333,1200,1201],{},"reason: 5",", because a model facing an unknown code doesn't stop, it guesses.",[147,1204,365],{"id":364},[152,1206,1207,1208,1211],{},"The hard part wasn't collection but ",[162,1209,1210],{},"retention",": keeping years of measurement\npoints without blowing up the database. The answer was progressive rollup —\nfine-grained data ages into hourly averages, then daily ones.",{"title":71,"searchDepth":191,"depth":191,"links":1213},[1214,1215,1216,1217,1218,1219],{"id":278,"depth":191,"text":279},{"id":1047,"depth":191,"text":1048},{"id":157,"depth":191,"text":158},{"id":1155,"depth":191,"text":1156},{"id":1176,"depth":191,"text":1177},{"id":364,"depth":191,"text":365},"\u002Fprojets\u002Fvigie-dashboard.png","Unified monitoring for my web projects — cookieless analytics, uptime checks, scheduled-job tracking and a public status page.",[1223,1226,1229],{"url":1224,"caption":1225},"\u002Fprojets\u002Fvigie-erreurs.png","Application errors — grouped by fingerprint, with latest occurrences and stack trace.",{"url":1227,"caption":1228},"\u002Fprojets\u002Fvigie-taches.png","Scheduled jobs — state, run history and an incident when one fails.",{"url":1230,"caption":1231},"\u002Fprojets\u002Fvigie-disponibilite.png","Uptime — URL checks with response time and 30-day history.",[1233,1234,1235,1236],"Multi-project with strict data isolation per API key","Cookieless visit analytics, so no consent banner","Alerts and an automatically generated public status page","Read-only MCP server — monitoring you can query from the editor",{},"\u002Fprojets\u002Fvigie",1,{"title":490,"description":1221},"https:\u002F\u002Fvigie.watch","projets\u002Fvigie",[28,16,18,34,37,51,1183],"RiBHY7jRZ-V3wJoap5kgVKpqBMn-pw7QB4wK5yabSNI",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":1246},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"\u002F>",1788530204342]